system-design52 min read

Design a Digital Lending Platform: The Complete Guide — A Senior+ Guide | Ayodhyya

Design a Digital Lending Platform: The Complete Guide

End-to-end architecture for personal loans, gold loans, home loans, and NBFC-grade digital lending at scale

Senior+ Guide 55+ min read 10,000+ words Ayodhyya

Table of Contents

  1. Introduction — The Digital Lending Landscape
  2. The Fintech Lending Revolution
  3. Functional and Non-Functional Requirements
  4. Capacity Estimation and Back-of-Envelope
  5. Data Model and Storage Schema
  6. High-Level Architecture
  7. API Design
  8. Loan Application Flow
  9. Credit Scoring and Underwriting (CIBIL / Experian)
  10. KYC and Document Verification
  11. Loan Approval Workflow
  12. Disbursement Pipeline
  13. EMI Collection and Repayment
  14. NPA Management and Collections
  15. Loan Product Variants
  16. Partner and DSA Portal
  17. Customer App and Self-Service
  18. Interest Calculation Engine
  19. Foreclosure and Prepayment
  20. Regulatory Compliance (RBI / NBFC)
  21. Fraud Detection
  22. Analytics and Reporting
  23. Cost Estimation
  24. Testing Strategy
  25. Interview Q&A

1. Introduction — The Digital Lending Landscape

Digital lending has fundamentally transformed how financial institutions originate, underwrite, and service loans. In India alone, the digital lending market is projected to surpass 1.3 trillion dollars by 2030, driven by smartphone penetration, UPI infrastructure, and regulatory frameworks established by the Reserve Bank of India. Companies like Bajaj Finserv, KreditBai, Lendingkart, and Zest AI have demonstrated that technology-first lending platforms can process thousands of loan applications per minute while maintaining rigorous risk management standards. The convergence of Aadhaar-based identity verification, Account Aggregator framework for consent-based data sharing, and real-time payment rails has made it possible to disburse loans in under five minutes from application to bank account credit.

A digital lending platform is not a single monolithic application. It is a constellation of microservices working in concert: a loan origination system that captures applications, a credit bureau integration layer that fetches scores from CIBIL, Experian, CRIF, and Equifax, a rules engine that evaluates creditworthiness, a document verification service that validates identity through Aadhaar eKYC and PAN verification, a disbursement engine that routes payments through NEFT, RTGS, or IMPS, and a collection system that manages EMI schedules, auto-debit mandates, and NPA escalation workflows. Each of these components must be designed for reliability, scalability, and regulatory compliance.

Designing such a platform requires deep understanding of distributed systems, event-driven architecture, financial regulations, and domain-driven design principles. This guide walks through every component of an enterprise-grade digital lending platform, from the initial customer application through credit scoring, loan approval, disbursement, repayment collection, and regulatory reporting. We will examine the system through the lens of building a platform that serves both NBFCs and banks, supporting multiple loan products including personal loans, gold loans, home loans, and business loans.

Interview Context: The digital lending platform design question tests your understanding of event-driven architecture, workflow orchestration, third-party integrations (credit bureaus, KYC providers, payment gateways), regulatory compliance, and financial domain modeling. It is an increasingly popular system design question at fintech companies like Stripe, Razorpay, Zest AI, Upstart, and LendingClub.

2. The Fintech Lending Revolution

The traditional lending process involved physical branch visits, paper-based document submission, manual underwriting that could take weeks, and opaque communication about application status. Digital lending compresses this timeline to minutes or hours. The key enablers of this transformation are Aadhaar-based eKYC for instant identity verification, Account Aggregator framework for consent-based financial data sharing, UPI and IMPS for instant disbursements, and APIs from credit bureaus that deliver credit scores within seconds.

The competitive landscape includes digital-first NBFCs like Lendingkart and Capital Float that originate loans entirely through mobile apps, traditional banks launching digital arms like HDFC Bank NeoBank and ICICI Bank InstaBIZ, peer-to-peer lending platforms like Faircent and i2iFunding, and embedded lending platforms that offer buy-now-pay-later through merchant partnerships. Each of these models requires different platform capabilities but shares core infrastructure for credit assessment, loan servicing, and compliance.

EraChannelProcessing TimeUnderwritingCustomer Experience
Traditional BankingBranch visit2 to 4 weeksManual, relationship-basedOpaque, phone follow-ups
Early Digital (2010-2015)Web portal3 to 7 daysSemi-automated rulesStatus page, email updates
Fintech Lending (2015-2020)Mobile-first1 to 3 daysML scoring, API-based bureauReal-time tracking, push notifications
Instant Lending (2020-present)Embedded, BNPLMinutes to hoursAI-driven, alternative dataPre-approved offers, one-tap disbursement

The evolution from traditional to instant lending has been driven by three forces: data availability through credit bureaus and Account Aggregators, computational power to run ML models in real-time, and regulatory frameworks that balance innovation with consumer protection. Understanding this evolution is essential for designing a platform that is both competitive today and adaptable to future regulatory and technological changes. The RBI Digital Lending Guidelines of September 2022 introduced strict requirements around loan disbursal destinations, grievance redressal, and data privacy that every platform must incorporate into its architecture from day one.

The Indian digital lending ecosystem is also shaped by unique infrastructure components. The India Stack comprising Aadhaar, UPI, and the Account Aggregator framework provides foundational building blocks that are not available in many other markets. Aadhaar covers over 1.3 billion residents and enables paperless eKYC in under 30 seconds. UPI processes over 10 billion transactions per month and provides real-time payment rails for both disbursement and collection. The Account Aggregator framework, with over 15 registered AAs, allows customers to securely share their financial data with lenders through a consent-based mechanism, eliminating the need for physical document submission.

3. Functional and Non-Functional Requirements

Functional Requirements

#RequirementPriorityDetails
F1Loan application submissionMustCustomer submits personal, financial, and employment details
F2KYC verificationMustAadhaar eKYC, PAN verification, document upload and OCR
F3Credit bureau integrationMustFetch CIBIL, Experian, CRIF scores and credit reports
F4Automated credit scoringMustML-based risk assessment with rules engine overlay
F5Loan approval workflowMustMulti-level approval: auto-approve, analyst review, credit committee
F6Loan disbursementMustNEFT, RTGS, IMPS, UPI disbursement with reconciliation
F7EMI calculation and schedulingMustReduce balance, flat rate, bullet repayment schedules
F8Repayment collectionMustNACH auto-debit, UPI mandate, manual payment
F9Foreclosure and prepaymentShouldPart-prepayment, full foreclosure with penal charge calculation
F10NPA managementMustDunning workflows, collection agent assignment, legal notice generation
F11Partner and DSA portalShouldLead submission, commission tracking, loan status visibility
F12Customer self-service portalShouldLoan status, repayment history, NOC download, statement generation
F13Regulatory reportingMustRBI returns, CRILC reporting, GST input credit reconciliation
F14Fraud detectionMustApplication fraud, identity fraud, income fraud detection
F15Analytics dashboardShouldPortfolio quality, disbursement trends, collection efficiency

Non-Functional Requirements

RequirementTargetRationale
Availability99.95%Financial services require high availability; downtime directly impacts revenue
Latency — application submissionLess than 2s P95Customer expects instant acknowledgment
Latency — credit score fetchLess than 5s P95Bureau API response time; must not block UI
Latency — disbursementLess than 30sCustomer expects quick fund transfer
Throughput10,000 applications per dayScalable for mid-size NBFC
Data durability99.999999%Financial records cannot be lost
EncryptionAES-256 at rest, TLS 1.3 in transitPCI DSS and RBI data localization requirements
Audit trailImmutable, 7-year retentionRegulatory requirement for all lending operations
RTO / RPORTO: 15 min, RPO: 0Zero data loss with quick recovery for disaster scenarios
Regulatory Note: RBI mandates that all customer data must be stored within India. Cloud deployments must use India-region data centers. Cross-border data transfer requires explicit consent and regulatory approval. All lending platforms must comply with the Digital Lending Guidelines issued by RBI in September 2022, which mandate that loan amounts must be disbursed directly to the borrower bank account and never to any third-party pool account or lending partner account.

4. Capacity Estimation and Back-of-Envelope

Traffic Assumptions

  • Daily loan applications: 10,000
  • Average loan tenure: 24 months
  • Active loans at any time: 500,000
  • Monthly EMI transactions: 500,000 (one per active loan per month)
  • Daily API requests: approximately 500,000 (applications, status checks, payments, reports)
  • Peak QPS: approximately 15 for application submission, approximately 50 for status checks, approximately 200 during EMI collection window

Storage Estimation

EntityRecord SizeRecords per YearAnnual Storage
Loan applications50 KB3,650,000approximately 175 GB
KYC documents2 MB3,650,000approximately 7 TB
EMI transaction logs2 KB6,000,000approximately 12 GB
Audit trail500 B50,000,000approximately 25 GB
Credit bureau reports100 KB3,650,000approximately 350 GB

Bandwidth Estimation

At 200 QPS with an average response size of 10 KB, the read bandwidth is approximately 2 MB/s or 170 GB/day. Write bandwidth is significantly lower at approximately 200 KB/s due to fewer write operations compared to reads. The document storage requirement of 7 TB annually demands object storage like S3 or Azure Blob with lifecycle policies to move older documents to cold storage after 12 months. For disaster recovery, we replicate critical data to a secondary AWS region within India with a Recovery Point Objective (RPO) of zero through synchronous replication for the primary database and near-synchronous replication for the document store.

Database Sizing

After 5 years of operation with 10,000 applications per day, the relational database holding loan metadata will grow to approximately 1.5 TB. This is manageable with proper sharding by loan account number and read replicas for analytics queries. The document store will hold approximately 35 TB after 5 years, requiring tiered storage with hot, warm, and cold tiers based on document access frequency. Hot tier for documents less than 90 days old uses SSD-backed storage, warm tier for documents 90 days to 1 year uses standard storage, and cold tier for documents older than 1 year uses Glacier-class storage for cost optimization.

5. Data Model and Storage Schema

The data model for a digital lending platform is complex because it must represent the entire lifecycle of a loan from application through disbursement, servicing, and closure. The core entities are Applicant, LoanApplication, LoanAccount, EMISchedule, Transaction, and Document. These entities have rich relationships and state machines that govern their transitions. The design uses event sourcing for the loan account aggregate, meaning every state change is captured as an immutable event that can be replayed for audit, debugging, or temporal queries required by regulators.

public class Applicant
{
    public Guid ApplicantId { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public string PANNumber { get; set; }
    public string AadhaarNumber { get; set; }
    public DateOnly DateOfBirth { get; set; }
    public string EmploymentType { get; set; }
    public decimal MonthlyIncome { get; set; }
    public string EmployerName { get; set; }
    public int CreditScore { get; set; }
    public string RiskCategory { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    public ICollection<LoanApplication> Applications { get; set; }
    public ICollection<KYCDocument> Documents { get; set; }
}

public class LoanApplication
{
    public Guid ApplicationId { get; set; }
    public Guid ApplicantId { get; set; }
    public string LoanProductType { get; set; }
    public decimal RequestedAmount { get; set; }
    public int TenureMonths { get; set; }
    public string Purpose { get; set; }
    public string Status { get; set; }
    public decimal? ApprovedAmount { get; set; }
    public decimal? ApprovedInterestRate { get; set; }
    public Guid? AssignedOfficerId { get; set; }
    public DateTime SubmittedAt { get; set; }
    public DateTime? DecisionedAt { get; set; }
    public string RejectionReason { get; set; }
    public ICollection<ApplicationEvent> AuditTrail { get; set; }
}

public class LoanAccount
{
    public Guid AccountId { get; set; }
    public Guid ApplicationId { get; set; }
    public string AccountNumber { get; set; }
    public Guid ApplicantId { get; set; }
    public string LoanProductType { get; set; }
    public decimal PrincipalAmount { get; set; }
    public decimal OutstandingPrincipal { get; set; }
    public decimal InterestRate { get; set; }
    public string InterestCalculationMethod { get; set; }
    public int TenureMonths { get; set; }
    public DateOnly DisbursalDate { get; set; }
    public DateOnly MaturityDate { get; set; }
    public decimal MonthlyEMI { get; set; }
    public int EMIPaid { get; set; }
    public int EMITotal { get; set; }
    public string AccountStatus { get; set; }
    public int DaysPastDue { get; set; }
    public DateTime CreatedAt { get; set; }
    public ICollection<EMISchedule> EMISchedule { get; set; }
    public ICollection<Transaction> Transactions { get; set; }
}

public class EMISchedule
{
    public Guid ScheduleId { get; set; }
    public Guid AccountId { get; set; }
    public int InstallmentNumber { get; set; }
    public DateOnly DueDate { get; set; }
    public decimal EMIAmount { get; set; }
    public decimal PrincipalComponent { get; set; }
    public decimal InterestComponent { get; set; }
    public decimal OutstandingBefore { get; set; }
    public decimal OutstandingAfter { get; set; }
    public string Status { get; set; }
    public DateOnly? PaidDate { get; set; }
    public decimal AmountPaid { get; set; }
    public decimal PenalCharges { get; set; }
}

public class Transaction
{
    public Guid TransactionId { get; set; }
    public Guid AccountId { get; set; }
    public string TransactionType { get; set; }
    public decimal Amount { get; set; }
    public string PaymentMethod { get; set; }
    public string ReferenceNumber { get; set; }
    public string Status { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    public string GatewayResponse { get; set; }
}

Database Technology Choices

Use CaseTechnologyRationale
Loan accounts, EMI schedulesPostgreSQLACID transactions, complex joins, JSON support
Application state machinePostgreSQL plus Event SourcingAudit trail, temporal queries, regulatory compliance
KYC documents, signed agreementsAWS S3 or Azure BlobDurability, lifecycle policies, compliance
Credit bureau responsesPostgreSQL plus Redis cacheCache bureau responses for 30 days to reduce API costs
Transaction logsPostgreSQL plus KafkaReal-time processing with durable event log
Analytics and reportingClickHouse or BigQueryColumnar storage for fast aggregations
Session and rate limitingRedisIn-memory performance for session management

6. High-Level Architecture

The digital lending platform follows a microservices architecture with event-driven communication between services. The core services are: Loan Origination Service, KYC Service, Credit Assessment Service, Underwriting Engine, Loan Booking Service, Disbursement Service, Collection Service, and Reporting Service. Each service owns its database and communicates through Kafka events for asynchronous workflows and REST or gRPC for synchronous queries.

graph TB subgraph "Customer Touchpoints" A[Mobile App] --> B[API Gateway] C[Web Portal] --> B D[Partner/DSA Portal] --> B end subgraph "Core Services" B --> E[Loan Origination Service] E --> F[KYC Service] E --> G[Credit Assessment Service] G --> H[Underwriting Engine] H --> I[Loan Booking Service] I --> J[Disbursement Service] J --> K[Collection Service] K --> L[NPA Management Service] end subgraph "External Integrations" F --> M[Aadhaar eKYC API] F --> N[PAN Verification API] G --> O[CIBIL Bureau API] G --> P[Experian Bureau API] J --> Q[NEFT/RTGS Gateway] J --> R[UPI Gateway] K --> S[NACH Gateway] end subgraph "Data Layer" E --> T[(PostgreSQL)] I --> T K --> T E --> U[Kafka Event Bus] I --> U K --> U T --> V[(ClickHouse Analytics)] end

The API Gateway handles authentication, rate limiting, request routing, and SSL termination. It routes requests to the appropriate microservice based on URL patterns and handles cross-cutting concerns like logging, metrics, and circuit breaking. The Loan Origination Service is the entry point for all loan applications and orchestrates the initial workflow of KYC verification and credit assessment before handing off to the Underwriting Engine.

Service Communication Patterns

  • Synchronous (gRPC): Credit score lookup, KYC status check, account balance verification — used when the caller needs an immediate response
  • Asynchronous (Kafka): Loan application submitted, KYC completed, credit report fetched, loan approved, disbursement initiated — used for workflow state transitions
  • Event Sourcing: Loan account state changes are stored as immutable events, enabling temporal queries and complete audit trails required by regulators
public class LoanOriginationOrchestrator
{
    private readonly IKycService _kycService;
    private readonly ICreditAssessmentService _creditService;
    private readonly IUnderwritingEngine _underwritingEngine;
    private readonly IKafkaProducer<string, LoanEvent> _eventProducer;
    private readonly ILogger<LoanOriginationOrchestrator> _logger;

    public async Task<LoanApplicationResult> ProcessApplicationAsync(
        LoanApplicationRequest request)
    {
        var application = new LoanApplication
        {
            ApplicationId = Guid.NewGuid(),
            ApplicantId = request.ApplicantId,
            LoanProductType = request.LoanProductType,
            RequestedAmount = request.RequestedAmount,
            TenureMonths = request.TenureMonths,
            Status = "Submitted",
            SubmittedAt = DateTime.UtcNow
        };

        await _eventProducer.ProduceAsync("loan-events",
            new LoanEvent
            {
                EventType = "ApplicationSubmitted",
                ApplicationId = application.ApplicationId,
                Payload = JsonSerializer.Serialize(application),
                Timestamp = DateTime.UtcNow
            });

        var kycResult = await _kycService.VerifyAsync(request.ApplicantId);
        if (!kycResult.IsVerified)
        {
            application.Status = "KYC_Failed";
            return new LoanApplicationResult
            {
                Success = false,
                Reason = $"KYC failed: {kycResult.FailureReason}"
            };
        }

        var creditReport = await _creditService.FetchCreditReportAsync(
            request.ApplicantId);

        var underwritingResult = await _underwritingEngine.EvaluateAsync(
            application, creditReport);

        application.Status = underwritingResult.IsApproved
            ? "Approved" : "Rejected";
        application.ApprovedAmount = underwritingResult.ApprovedAmount;
        application.ApprovedInterestRate = underwritingResult.InterestRate;
        application.DecisionedAt = DateTime.UtcNow;

        await _eventProducer.ProduceAsync("loan-events",
            new LoanEvent
            {
                EventType = underwritingResult.IsApproved
                    ? "ApplicationApproved" : "ApplicationRejected",
                ApplicationId = application.ApplicationId,
                Payload = JsonSerializer.Serialize(underwritingResult),
                Timestamp = DateTime.UtcNow
            });

        return new LoanApplicationResult
        {
            Success = underwritingResult.IsApproved,
            Application = application,
            UnderwritingResult = underwritingResult
        };
    }
}

7. API Design

The API design follows RESTful conventions with consistent error handling, pagination, and versioning. All endpoints require JWT authentication with role-based access control. The API versioning strategy uses URL path prefixing (v1, v2) to support backward compatibility during platform upgrades.

MethodEndpointDescriptionAuth
POST/v1/applicationsSubmit new loan applicationCustomer JWT
GET/v1/applications/{id}Get application status and detailsCustomer JWT
PUT/v1/applications/{id}/documentsUpload KYC documentsCustomer JWT
POST/v1/applications/{id}/submitSubmit completed applicationCustomer JWT
GET/v1/loans/{accountNumber}Get loan account detailsCustomer JWT
GET/v1/loans/{accountNumber}/scheduleGet EMI repayment scheduleCustomer JWT
POST/v1/loans/{accountNumber}/prepayInitiate prepaymentCustomer JWT
POST/v1/loans/{accountNumber}/forecloseInitiate foreclosureCustomer JWT
GET/v1/loans/{accountNumber}/statementsDownload loan statementCustomer JWT
POST/v1/admin/applications/{id}/approveApprove application (officer)Officer JWT
POST/v1/admin/applications/{id}/rejectReject application (officer)Officer JWT
POST/v1/disburse/{accountNumber}Initiate disbursementOfficer JWT
GET/v1/partner/leadsGet partner leadsPartner JWT
POST/v1/partner/leadsSubmit new leadPartner JWT
GET/v1/admin/reports/portfolioPortfolio quality reportAdmin JWT
GET/v1/admin/reports/crilcCRILC regulatory reportAdmin JWT

Error Response Format

public class ApiResponse<T>
{
    public bool Success { get; set; }
    public T Data { get; set; }
    public string ErrorCode { get; set; }
    public string ErrorMessage { get; set; }
    public List<ValidationError> ValidationErrors { get; set; }
    public string TraceId { get; set; }
    public DateTime Timestamp { get; set; }
}

public static class LendingErrorCodes
{
    public const string INVALID_PAN = "LEND_001";
    public const string KYC_FAILED = "LEND_002";
    public const string CREDIT_SCORE_LOW = "LEND_003";
    public const string INCOME_INSUFFICIENT = "LEND_004";
    public const string EXISTING_NPA = "LEND_005";
    public const string DUPLICATE_APPLICATION = "LEND_006";
    public const string DISBURSEMENT_FAILED = "LEND_007";
    public const string MANDATE_NOT_ACTIVE = "LEND_008";
    public const string DOCUMENT_EXPIRED = "LEND_009";
    public const string AMOUNT_EXCEEDS_LIMIT = "LEND_010";
}

8. Loan Application Flow

The loan application flow is a multi-step wizard that captures applicant information, validates it in real-time, and progresses through a state machine. The application starts as a Draft when the customer begins entering information, transitions to Submitted when all required fields are complete, moves through KYC_Pending and UnderReview, and finally reaches either Approved, Rejected, or Cancelled. Each state transition generates an event that is published to Kafka for downstream processing and audit logging.

stateDiagram-v2 [*] --> Draft Draft --> Submitted: Customer submits Submitted --> KYC_Pending: KYC initiated KYC_Pending --> KYC_Verified: KYC passed KYC_Pending --> KYC_Failed: KYC failed KYC_Verified --> UnderReview: Credit check initiated UnderReview --> AutoApproved: Rules engine approves UnderReview --> ManualReview: Needs human review ManualReview --> AutoApproved: Officer approves ManualReview --> Rejected: Officer rejects AutoApproved --> DisbursementPending: Booking complete DisbursementPending --> Disbursed: Funds transferred Disbursed --> Active: First EMI pending Active --> Closed: All EMIs paid Active --> NPA: DPD greater than 90 NPA --> WrittenOff: Write-off approved KYC_Failed --> Rejected Rejected --> [*] Closed --> [*]

Application Form Fields

The application form is designed to minimize friction while collecting all information needed for underwriting. The form uses progressive profiling: basic details are collected first (name, phone, email, loan amount), followed by PAN verification that auto-populates income data from credit bureau records, then employment details, and finally document upload. Real-time validation ensures that PAN format, Aadhaar format, and phone number format are correct before the customer proceeds to the next step.

public class LoanApplicationStateMachine
{
    private readonly Dictionary<string, HashSet<string>> _validTransitions = new()
    {
        ["Draft"] = new() { "Submitted", "Cancelled" },
        ["Submitted"] = new() { "KYC_Pending", "Cancelled" },
        ["KYC_Pending"] = new() { "KYC_Verified", "KYC_Failed" },
        ["KYC_Verified"] = new() { "UnderReview" },
        ["UnderReview"] = new() { "AutoApproved", "ManualReview", "Rejected" },
        ["ManualReview"] = new() { "AutoApproved", "Rejected" },
        ["AutoApproved"] = new() { "DisbursementPending" },
        ["DisbursementPending"] = new() { "Disbursed" },
        ["Disbursed"] = new() { "Active", "Closed" },
        ["Active"] = new() { "Closed", "NPA" },
        ["NPA"] = new() { "WrittenOff", "Restructured", "Active" },
        ["KYC_Failed"] = new() { "Rejected" },
        ["Rejected"] = new() { "Draft" },
    };

    public bool CanTransition(string currentStatus, string targetStatus)
    {
        return _validTransitions.ContainsKey(currentStatus) &&
               _validTransitions[currentStatus].Contains(targetStatus);
    }

    public async Task<TransitionResult> TransitionAsync(
        LoanApplication application, string targetStatus,
        Guid operatorId, string reason)
    {
        if (!CanTransition(application.Status, targetStatus))
        {
            return new TransitionResult
            {
                Success = false,
                Error = $"Invalid transition from {application.Status} to {targetStatus}"
            };
        }

        var previousStatus = application.Status;
        application.Status = targetStatus;
        application.UpdatedAt = DateTime.UtcNow;

        var auditEvent = new ApplicationEvent
        {
            EventId = Guid.NewGuid(),
            ApplicationId = application.ApplicationId,
            PreviousStatus = previousStatus,
            NewStatus = targetStatus,
            OperatorId = operatorId,
            Reason = reason,
            Timestamp = DateTime.UtcNow
        };

        await SaveAuditEventAsync(auditEvent);
        await PublishTransitionEventAsync(auditEvent);

        return new TransitionResult { Success = true, Event = auditEvent };
    }
}

9. Credit Scoring and Underwriting (CIBIL / Experian)

Credit scoring is the backbone of digital lending. The platform integrates with multiple credit bureaus to fetch credit scores and detailed credit reports. In India, the four major credit bureaus are CIBIL (TransUnion), Experian, CRIF High Mark, and Equifax. Each bureau provides a credit score typically in the range of 300 to 900, a credit report with trade lines showing existing loans and credit cards, inquiries made by other lenders, and public records including defaults and legal cases.

The credit assessment service fetches reports from multiple bureaus in parallel, normalizes the data into a common schema, and feeds it to the underwriting engine. The underwriting engine uses a combination of rule-based checks (minimum score thresholds, maximum DPD, income-to-EMI ratio) and machine learning models (gradient boosted trees trained on historical loan performance data) to arrive at a credit decision. The ML model is retrained monthly on the latest loan performance data, with careful monitoring for model drift and bias. Feature engineering includes over 200 features derived from the credit report, bank statement analysis, application form data, and device fingerprinting signals.

BureauScore RangeAPI LatencyCost per FetchData Richness
CIBIL (TransUnion)300 to 9002 to 5s15 to 25 INRHigh — most widely used in India
Experian India300 to 9002 to 4s15 to 20 INRHigh — detailed trade line data
CRIF High Mark300 to 9001 to 3s10 to 15 INRMedium — growing market share
Equifax India300 to 9002 to 5s15 to 20 INRMedium — strong in commercial lending
public class CreditAssessmentService
{
    private readonly ICibilClient _cibilClient;
    private readonly IExperianClient _experianClient;
    private readonly ICrifClient _crifClient;
    private readonly IRedisCache _cache;
    private readonly IKafkaProducer<string, CreditEvent> _producer;

    public async Task<CreditReport> FetchCreditReportAsync(Guid applicantId)
    {
        var cacheKey = $"credit_report:{applicantId}";
        var cached = await _cache.GetAsync<CreditReport>(cacheKey);
        if (cached != null) return cached;

        var applicant = await GetApplicantAsync(applicantId);
        var cibilTask = _cibilClient.GetReportAsync(
            applicant.PANNumber, applicant.AadhaarNumber);
        var experianTask = _experianClient.GetReportAsync(
            applicant.PANNumber, applicant.Phone);
        var crifTask = _crifClient.GetReportAsync(
            applicant.PANNumber, applicant.FullName);

        await Task.WhenAll(cibilTask, experianTask, crifTask);

        var normalized = NormalizeCreditReport(
            cibilTask.Result, experianTask.Result, crifTask.Result);

        await _cache.SetAsync(cacheKey, normalized, TimeSpan.FromDays(30));

        await _producer.ProduceAsync("credit-events", new CreditEvent
        {
            EventType = "CreditReportFetched",
            ApplicantId = applicantId,
            CibilScore = normalized.CibilScore,
            ExperianScore = normalized.ExperianScore,
            Timestamp = DateTime.UtcNow
        });

        return normalized;
    }

    private CreditReport NormalizeCreditReport(
        CibilResponse cibil, ExperianResponse experian, CrifResponse crif)
    {
        return new CreditReport
        {
            CibilScore = cibil?.Score ?? 0,
            ExperianScore = experian?.Score ?? 0,
            CrifScore = crif?.Score ?? 0,
            CompositeScore = CalculateCompositeScore(cibil, experian, crif),
            TotalActiveLoans = MergeActiveLoans(cibil, experian, crif),
            MaxDPD = GetMaxDaysPastDue(cibil, experian, crif),
            TotalOutstanding = MergeOutstandingAmounts(cibil, experian, crif),
            RecentInquiries = MergeInquiries(cibil, experian, crif),
            NegativeAccounts = IdentifyNegativeAccounts(cibil, experian, crif)
        };
    }
}

Underwriting Rules Engine

RulePersonal LoanGold LoanHome LoanImpact
Minimum Credit Score650500700Hard reject if below
Maximum DPD in Last 12 Months0 days30 days0 daysHard reject if exceeded
FOIR (Fixed Obligation to Income Ratio)Up to 50%Up to 60%Up to 50%Affects approved amount
Minimum Monthly Income25,000 INR15,000 INR50,000 INRHard reject if below
Maximum Loan Amount25,00,000 INRGold value x 75%Property value x 80%Cap on approved amount
Minimum Vintage (employment)6 monthsN/A24 monthsHard reject if below
Existing NPA CheckNo active NPANo active NPANo active NPAHard reject if active NPA
public class UnderwritingEngine
{
    private readonly IRulesEngine _rulesEngine;
    private readonly IMLScoringModel _mlModel;
    private readonly ILogger<UnderwritingEngine> _logger;

    public async Task<UnderwritingResult> EvaluateAsync(
        LoanApplication application, CreditReport creditReport)
    {
        var ruleResults = await _rulesEngine.EvaluateAsync(
            new RuleContext
            {
                LoanProduct = application.LoanProductType,
                RequestedAmount = application.RequestedAmount,
                TenureMonths = application.TenureMonths,
                CreditScore = creditReport.CompositeScore,
                MaxDPD = creditReport.MaxDPD,
                TotalOutstanding = creditReport.TotalOutstanding,
                MonthlyIncome = application.Applicant.MonthlyIncome,
                ExistingEMIs = creditReport.TotalActiveLoans
                    .Sum(l => l.MonthlyEMI),
                NegativeAccounts = creditReport.NegativeAccounts.Count
            });

        if (ruleResults.Any(r => r.IsCritical && !r.Passed))
        {
            return new UnderwritingResult
            {
                IsApproved = false,
                RejectionReason = ruleResults
                    .First(r => r.IsCritical && !r.Passed).RuleName,
                RuleResults = ruleResults
            };
        }

        var mlInput = new MlScoringInput
        {
            CreditScore = creditReport.CompositeScore,
            MonthlyIncome = application.Applicant.MonthlyIncome,
            RequestedAmount = application.RequestedAmount,
            TenureMonths = application.TenureMonths,
            FOIR = CalculateFOIR(application, creditReport),
            EmploymentType = application.Applicant.EmploymentType,
            Age = application.Applicant.DateOfBirth.Age,
            CityTier = GetCityTier(application.Applicant.City),
            LoanProductRisk = GetProductRiskWeight(application.LoanProductType)
        };

        var mlScore = await _mlModel.PredictAsync(mlInput);

        var approvedAmount = CalculateApprovedAmount(
            application.RequestedAmount, mlScore, ruleResults);
        var interestRate = CalculateInterestRate(
            mlScore, application.LoanProductType, application.TenureMonths);

        return new UnderwritingResult
        {
            IsApproved = approvedAmount > 0,
            ApprovedAmount = approvedAmount,
            InterestRate = interestRate,
            MLRiskScore = mlScore.RiskScore,
            RiskCategory = mlScore.RiskCategory,
            RuleResults = ruleResults
        };
    }
}

10. KYC and Document Verification

Know Your Customer verification is a regulatory mandate under RBI guidelines. The platform must verify the applicant identity through Aadhaar eKYC (demographic or biometric), verify PAN through NSDL or UTIITSL, and optionally verify income through bank statement analysis using the Account Aggregator framework. Document verification uses OCR and AI to extract data from uploaded documents and validate them against the application form data.

KYC Verification Steps

  1. Aadhaar eKYC: Send OTP to Aadhaar-linked mobile number or use biometric authentication. Verify name, date of birth, address, and photo from Aadhaar database.
  2. PAN Verification: Validate PAN number format and verify name and date of birth against NSDL database. This confirms the applicant tax identity and is mandatory for loans above 50,000 INR.
  3. Bank Statement Analysis: Use Account Aggregator framework to fetch 6 to 12 months of bank statements with customer consent. Analyze income patterns, existing EMIs, average balance, and spending patterns.
  4. Document OCR: Process uploaded salary slips, bank statements, and property documents using OCR to extract key data points for validation.
  5. Video KYC: For higher-value loans, conduct live video verification where the customer shows their original documents to a verification agent.
public class KycVerificationService
{
    private readonly IAadhaarClient _aadhaarClient;
    private readonly IPanVerificationClient _panClient;
    private readonly IAccountAggregatorClient _aaClient;
    private readonly IDocumentOcrService _ocrService;
    private readonly IKycRepository _kycRepository;

    public async Task<KycResult> VerifyApplicantAsync(Guid applicantId)
    {
        var applicant = await GetApplicantAsync(applicantId);
        var results = new List<VerificationStep>();

        var aadhaarTask = VerifyAadhaarAsync(applicant);
        var panTask = VerifyPanAsync(applicant);

        await Task.WhenAll(aadhaarTask, panTask);

        results.Add(aadhaarTask.Result);
        results.Add(panTask.Result);

        if (applicant.ConsentForBankStatements)
        {
            var bankStatements = await _aaClient.FetchStatementsAsync(
                applicant.AccountAggregatorHandle, months: 6);
            var incomeAnalysis = AnalyzeBankStatements(bankStatements);
            results.Add(new VerificationStep
            {
                Step = "BankStatementAnalysis",
                Passed = incomeAnalysis.VerifiedIncome >= applicant.MonthlyIncome * 0.8m,
                ExtractedData = incomeAnalysis
            });
        }

        var allPassed = results.All(r => r.Passed);
        var kycResult = new KycResult
        {
            ApplicantId = applicantId,
            IsVerified = allPassed,
            VerificationSteps = results,
            VerifiedAt = DateTime.UtcNow,
            ExpiryDate = DateTime.UtcNow.AddYears(2)
        };

        await _kycRepository.SaveResultAsync(kycResult);
        return kycResult;
    }
}

11. Loan Approval Workflow

The loan approval workflow handles the decision-making process after credit assessment. For digital lending, the workflow is designed to maximize straight-through processing (STP) where loans are approved or rejected automatically without human intervention. Depending on the loan product and risk category, applications may require manual review by a credit analyst or credit committee approval for high-value or borderline cases.

Approval Tiers

TierLoan AmountRisk CategoryApproval AuthoritySLA
Auto-ApproveUp to 2,00,000 INRLow RiskSystem (rules engine + ML)Instant
Analyst Review2,00,000 to 10,00,000 INRMedium RiskCredit Analyst4 hours
Senior Analyst10,00,000 to 25,00,000 INRMedium RiskSenior Credit Officer8 hours
Credit CommitteeAbove 25,00,000 INRAnyCredit Committee (3 members)24 hours
Exception HandlingAny amountHigh RiskRisk Committee48 hours
public class LoanApprovalOrchestrator
{
    private readonly ILoanApplicationRepository _appRepository;
    private readonly ITaskAssignmentService _taskService;
    private readonly IKafkaProducer<string, ApprovalEvent> _producer;

    public async Task<ApprovalResult> RouteForApprovalAsync(
        LoanApplication application, UnderwritingResult underwritingResult)
    {
        var approvalTier = DetermineApprovalTier(
            application.RequestedAmount, underwritingResult.RiskCategory);

        switch (approvalTier)
        {
            case ApprovalTier.AutoApprove:
                return await ProcessAutoApprovalAsync(
                    application, underwritingResult);
            case ApprovalTier.AnalystReview:
            case ApprovalTier.SeniorAnalyst:
                return await AssignToAnalystAsync(
                    application, underwritingResult, approvalTier);
            case ApprovalTier.CreditCommittee:
                return await EscalateToCommitteeAsync(
                    application, underwritingResult);
            default:
                throw new InvalidOperationException(
                    $"Unknown approval tier: {approvalTier}");
        }
    }

    private async Task<ApprovalResult> ProcessAutoApprovalAsync(
        LoanApplication application, UnderwritingResult result)
    {
        application.Status = "AutoApproved";
        application.ApprovedAmount = result.ApprovedAmount;
        application.ApprovedInterestRate = result.InterestRate;
        application.DecisionedAt = DateTime.UtcNow;

        await _appRepository.UpdateAsync(application);
        var loanAccount = await CreateLoanAccountAsync(application);

        await _producer.ProduceAsync("approval-events", new ApprovalEvent
        {
            EventType = "LoanAutoApproved",
            ApplicationId = application.ApplicationId,
            AccountId = loanAccount.AccountId,
            ApprovedAmount = result.ApprovedAmount,
            InterestRate = result.InterestRate,
            Timestamp = DateTime.UtcNow
        });

        await NotifyCustomerAsync(application.ApplicantId,
            $"Congratulations! Your loan of {result.ApprovedAmount:N0} INR has been approved at {result.InterestRate}% per annum.");

        return new ApprovalResult
        {
            Approved = true,
            ApprovalTier = ApprovalTier.AutoApprove,
            LoanAccount = loanAccount
        };
    }
}

12. Disbursement Pipeline

Disbursement is the process of transferring the approved loan amount to the applicant bank account. This is a critical step that requires integration with payment gateways and careful handling of reconciliation. The disbursement pipeline ensures that funds are transferred only after all pre-disbursement checks are complete: loan agreement is signed, insurance is activated (if mandatory), first EMI mandate is registered, and all conditions precedent are satisfied. The RBI Digital Lending Guidelines mandate that the loan amount must be disbursed directly to the borrower bank account.

Pre-Disbursement Checklist

#CheckVerificationMandatory
1Loan agreement signedE-Signature or DSC verificationYes
2KYC freshKYC expiry date checkYes
3Credit report freshBureau data within 30 daysYes
4NACH mandate registeredMandate status equals ActiveYes
5Insurance activatedPolicy number generatedConditional
6Property valuation (home loan)Valuation report approvedHome Loan only
7Gold appraisal (gold loan)Appraisal report, gold purity verifiedGold Loan only
8Income re-verificationLatest salary slip or ITRAmount above 10 Lakhs
public class DisbursementService
{
    private readonly IDisbursementRepository _disbursementRepo;
    private readonly IPaymentGateway _paymentGateway;
    private readonly ILoanAccountRepository _accountRepo;
    private readonly IKafkaProducer<string, DisbursementEvent> _producer;

    public async Task<DisbursementResult> DisburseAsync(
        Guid accountId, Guid officerId)
    {
        var account = await _accountRepo.GetByIdAsync(accountId);

        var checklist = await RunPreDisbursementChecksAsync(account);
        if (checklist.HasFailures)
        {
            return new DisbursementResult
            {
                Success = false,
                FailedChecks = checklist.Failures
            };
        }

        var disbursement = new Disbursement
        {
            DisbursementId = Guid.NewGuid(),
            AccountId = accountId,
            Amount = account.PrincipalAmount,
            DisbursedTo = account.Applicant.BankAccountNumber,
            BankIFSC = account.Applicant.BankIFSC,
            Status = "Initiated",
            InitiatedBy = officerId,
            InitiatedAt = DateTime.UtcNow
        };

        var paymentResult = await _paymentGateway.TransferAsync(
            new PaymentRequest
            {
                Amount = disbursement.Amount,
                DestinationAccount = disbursement.DisbursedTo,
                DestinationIFSC = disbursement.BankIFSC,
                Reference = account.AccountNumber,
                Narration = $"Loan Disbursement - {account.AccountNumber}"
            });

        if (paymentResult.Success)
        {
            disbursement.Status = "Completed";
            disbursement.UtrNumber = paymentResult.UtrNumber;
            disbursement.CompletedAt = DateTime.UtcNow;

            account.OutstandingPrincipal = account.PrincipalAmount;
            account.DisbursalDate = DateOnly.FromDateTime(DateTime.UtcNow);
            account.MaturityDate = account.DisbursalDate.AddMonths(
                account.TenureMonths);
            account.AccountStatus = "Active";
            await _accountRepo.UpdateAsync(account);

            await GenerateEmiScheduleAsync(account);

            await _producer.ProduceAsync("disbursement-events",
                new DisbursementEvent
                {
                    EventType = "DisbursementCompleted",
                    AccountId = accountId,
                    Amount = disbursement.Amount,
                    UtrNumber = paymentResult.UtrNumber,
                    Timestamp = DateTime.UtcNow
                });
        }
        else
        {
            disbursement.Status = "Failed";
            disbursement.FailureReason = paymentResult.FailureReason;
        }

        await _disbursementRepo.SaveAsync(disbursement);
        return new DisbursementResult
        {
            Success = paymentResult.Success,
            Disbursement = disbursement
        };
    }
}

13. EMI Collection and Repayment

EMI collection is the process of recovering monthly installments from the borrower. The primary mechanism in India is NACH (National Automated Clearing House) auto-debit mandates, where the borrower authorizes the lender to debit their bank account on the EMI due date. UPI autopay mandates are increasingly popular as an alternative due to faster settlement and higher success rates. The collection service manages mandate registration, generates NACH files, processes payment confirmations, handles bounce cases, and triggers dunning workflows for overdue payments.

Collection Methods

MethodSettlement TimeSuccess RateCostBest For
NACH Auto-DebitT+1 to T+285 to 90%1 to 3 INR per txRegular monthly EMIs
UPI AutopaySame day90 to 95%Free or minimalTech-savvy customers
NEFT or RTGST+0 to T+199%2 to 5 INR per txForeclosure, large payments
Manual Payment (App)Instant95%Gateway chargesSelf-service customers
Cash CollectionT+1VariableAgent commissionRural areas, NPA accounts
public class CollectionService
{
    private readonly ILoanAccountRepository _accountRepo;
    private readonly INachGateway _nachGateway;
    private readonly IUpiGateway _upiGateway;
    private readonly IKafkaProducer<string, CollectionEvent> _producer;

    public async Task<CollectionResult> ProcessEmiCollectionAsync(
        Guid accountId)
    {
        var account = await _accountRepo.GetWithScheduleAsync(accountId);
        var dueEmi = account.EMISchedule
            .FirstOrDefault(e => e.Status == "Pending" &&
                e.DueDate <= DateOnly.FromDateTime(DateTime.UtcNow));

        if (dueEmi == null)
            return new CollectionResult { Status = "No EMI Due" };

        var collectionAttempt = new CollectionAttempt
        {
            AttemptId = Guid.NewGuid(),
            AccountId = accountId,
            ScheduleId = dueEmi.ScheduleId,
            Amount = dueEmi.EMIAmount,
            AttemptedAt = DateTime.UtcNow
        };

        var nachResult = await _nachGateway.InitiateDebitAsync(
            new NachDebitRequest
            {
                MandateId = account.ActiveMandateId,
                Amount = dueEmi.EMIAmount,
                Reference = $"{account.AccountNumber}-EMI-{dueEmi.InstallmentNumber}"
            });

        if (nachResult.Success)
        {
            await MarkEmiPaidAsync(dueEmi, nachResult.SettlementReference);
            collectionAttempt.Method = "NACH";
            collectionAttempt.Status = "Success";
        }
        else
        {
            var upiResult = await _upiGateway.InitiateAutopayAsync(
                new UpiAutopayRequest
                {
                    MandateId = account.UpiMandateId,
                    Amount = dueEmi.EMIAmount,
                    Reference = $"{account.AccountNumber}-EMI-{dueEmi.InstallmentNumber}"
                });

            if (upiResult.Success)
            {
                await MarkEmiPaidAsync(dueEmi, upiResult.TransactionId);
                collectionAttempt.Method = "UPI";
                collectionAttempt.Status = "Success";
            }
            else
            {
                collectionAttempt.Method = "NACH+UPI";
                collectionAttempt.Status = "Bounced";
                collectionAttempt.BounceReason = upiResult.FailureReason;
                await TriggerDunningAsync(account, dueEmi);

                await _producer.ProduceAsync("collection-events",
                    new CollectionEvent
                    {
                        EventType = "EmiBounced",
                        AccountId = accountId,
                        InstallmentNumber = dueEmi.InstallmentNumber,
                        Amount = dueEmi.EMIAmount,
                        Timestamp = DateTime.UtcNow
                    });
            }
        }

        return new CollectionResult
        {
            Status = collectionAttempt.Status,
            Attempt = collectionAttempt
        };
    }
}

14. NPA Management and Collections

Non-Performing Asset management is a critical component of any lending platform. An asset is classified as NPA when the borrower fails to make interest or principal payments for more than 90 days as per RBI guidelines. The platform must implement automated dunning workflows that escalate based on the Days Past Due (DPD) count, assign collection agents, generate legal notices, and track recovery efforts. Effective NPA management directly impacts the lender portfolio quality, provisioning requirements, and profitability.

NPA Classification and Escalation

DPD RangeClassificationActionEscalation Level
1 to 30 daysStandard (Special Mention)SMS, Email, IVR remindersAutomated dunning
31 to 60 daysSub-standardPhone calls by collection agentLevel 1 collection team
61 to 90 daysDoubtfulField visit plus legal noticeLevel 2 collection team
91 to 180 daysNPAFormal NPA classification, SARFAESI noticeRecovery team plus Legal
181 to 365 daysNPAOne-Time Settlement offers, legal proceedingsLegal team plus Management
365 plus daysWrite-off candidateWrite-off recommendation, continued recoveryRisk Committee
public class NpaManagementService
{
    private readonly ILoanAccountRepository _accountRepo;
    private readonly ICollectionAgentService _agentService;
    private readonly INotificationService _notificationService;
    private readonly ILegalNoticeService _legalService;

    public async Task ProcessDailyDpdUpdateAsync()
    {
        var activeLoans = await _accountRepo.GetActiveLoansAsync();

        foreach (var loan in activeLoans)
        {
            var overdueEmis = loan.EMISchedule
                .Where(e => e.Status == "Pending" &&
                    e.DueDate < DateOnly.FromDateTime(DateTime.UtcNow))
                .ToList();

            if (!overdueEmis.Any()) continue;

            var oldestOverdue = overdueEmis.Min(e => e.DueDate);
            var dpd = (DateTime.UtcNow -
                oldestOverdue.ToDateTime(TimeOnly.MinValue)).Days;

            var previousDpd = loan.DaysPastDue;
            loan.DaysPastDue = dpd;

            if (dpd > 90 && previousDpd <= 90)
            {
                loan.AccountStatus = "NPA";
                await ClassifyAsNpaAsync(loan, dpd);
            }
            else if (dpd > 30 && previousDpd <= 30)
            {
                await EscalateCollectionAsync(loan, CollectionLevel.Level1);
            }
            else if (dpd > 60 && previousDpd <= 60)
            {
                await EscalateCollectionAsync(loan, CollectionLevel.Level2);
                await _legalService.SendLegalNoticeAsync(loan);
            }

            if (dpd <= 30)
                await SendRemindersAsync(loan, dpd);

            await _accountRepo.UpdateAsync(loan);
        }
    }
}

One-Time Settlement (OTS) Engine

For NPA accounts, the platform supports One-Time Settlement offers where the borrower pays a lump sum that is less than the total outstanding amount. The OTS engine calculates the settlement amount based on the outstanding principal, accrued interest, penal charges, and the probability of recovery. The settlement offer must be approved by authorized personnel based on the settlement amount thresholds: up to 10 percent hair-cut requires branch manager approval, 10 to 25 percent requires zonal head approval, and above 25 percent requires credit committee approval.

15. Loan Product Variants — Gold, Personal, Home

A production lending platform must support multiple loan products, each with distinct underwriting criteria, documentation requirements, and servicing workflows. The three most common products in Indian digital lending are personal loans, gold loans, and home loans. Each product has its own risk profile, regulatory requirements, and operational processes. The platform uses a strategy pattern to encapsulate product-specific logic, allowing new loan products to be added without modifying core services.

FeaturePersonal LoanGold LoanHome Loan
CollateralUnsecuredGold ornamentsProperty (real estate)
Max Amount25,00,000 INRGold value x 75%Property value x 80%
Tenure12 to 60 months6 to 36 months10 to 30 years
Interest Rate10.5 to 24% p.a.7 to 12% p.a.8.5 to 11% p.a.
Processing Time24 to 72 hours1 to 4 hours7 to 21 days
KYC RequirementStandard eKYCStandard eKYCEnhanced KYC plus property docs
Income ProofRequiredOptional (LTV-based)Required (ITR plus salary slips)
Property AssessmentN/AGold appraisalLegal plus technical valuation
InsuranceOptionalOptionalMandatory (property plus life)
Foreclosure Penalty2 to 5%Nil to 1%Nil to 2%
public abstract class LoanProductStrategy
{
    public abstract string ProductType { get; }
    public abstract Task<ValidationResult> ValidateApplicationAsync(
        LoanApplication application);
    public abstract Task<UnderwritingResult> UnderwriteAsync(
        LoanApplication application, CreditReport creditReport);
    public abstract Task<PreDisbursementChecklist> GetPreDisbursementChecksAsync(
        LoanAccount account);
    public abstract decimal CalculateForeclosureCharges(
        LoanAccount account, decimal outstandingPrincipal);
}

public class GoldLoanStrategy : LoanProductStrategy
{
    public override string ProductType => "Gold";

    public override async Task<ValidationResult> ValidateApplicationAsync(
        LoanApplication application)
    {
        var errors = new List<string>();

        if (application.CollateralDetails == null)
            errors.Add("Gold collateral details are required");

        if (application.CollateralDetails?.GoldWeightGrams == null ||
            application.CollateralDetails.GoldWeightGrams < 10)
            errors.Add("Minimum gold weight is 10 grams");

        if (application.CollateralDetails?.PurityKarat == null ||
            application.CollateralDetails.PurityKarat < 18)
            errors.Add("Minimum gold purity is 18 karat");

        var goldValue = application.CollateralDetails?.GoldWeightGrams *
            GetGoldRatePerGram(application.CollateralDetails.PurityKarat);
        var maxLoanAmount = goldValue * 0.75m;

        if (application.RequestedAmount > maxLoanAmount)
            errors.Add($"Maximum loan amount for declared gold is {maxLoanAmount:N0} INR");

        return new ValidationResult
        {
            IsValid = !errors.Any(),
            Errors = errors
        };
    }

    public override async Task<UnderwritingResult> UnderwriteAsync(
        LoanApplication application, CreditReport creditReport)
    {
        var goldValue = CalculateGoldValue(application.CollateralDetails);
        var ltv = application.RequestedAmount / goldValue;
        var approvedAmount = goldValue * 0.75m;

        var isEligible = creditReport.CompositeScore >= 500 ||
                         creditReport.CompositeScore == 0;

        return new UnderwritingResult
        {
            IsApproved = isEligible,
            ApprovedAmount = Math.Min(approvedAmount, application.RequestedAmount),
            InterestRate = 8.5m,
            RiskCategory = ltv > 0.6m ? "Medium" : "Low"
        };
    }
}

public class HomeLoanStrategy : LoanProductStrategy
{
    public override string ProductType => "Home";

    public override async Task<ValidationResult> ValidateApplicationAsync(
        LoanApplication application)
    {
        var errors = new List<string>();

        if (application.PropertyDetails == null)
            errors.Add("Property details are mandatory for home loans");

        if (application.IncomeDocuments == null ||
            !application.IncomeDocuments.Any())
            errors.Add("Income documents (ITR, salary slips) are required");

        if (application.Applicant.MonthlyIncome < 50000)
            errors.Add("Minimum monthly income of 50,000 INR required");

        var employmentMonths = CalculateEmploymentVintage(
            application.Applicant.EmploymentStartDate);
        if (employmentMonths < 24)
            errors.Add("Minimum 24 months of employment required");

        return new ValidationResult
        {
            IsValid = !errors.Any(),
            Errors = errors
        };
    }
}

16. Partner and DSA Portal

Distribution Service Agents (DSAs) and channel partners are critical for loan origination at scale. The partner portal provides DSAs with a self-service interface to submit leads, track application status, view commission statements, and access marketing materials. The portal must support role-based access control with partner admin, DSA agent, and sub-agent roles, lead deduplication to prevent multiple agents from submitting the same customer, and real-time status tracking so DSAs can follow up with applicants.

DSA Commission Structure

Loan ProductCommission RateDisbursement LinkagePayment Cycle
Personal Loan1.5 to 3% of disbursed amountOn disbursementMonthly
Gold Loan0.5 to 1% of disbursed amountOn disbursementMonthly
Home Loan0.5 to 1.5% of disbursed amountOn disbursementMonthly
Business Loan2 to 4% of disbursed amountOn disbursementMonthly
public class PartnerService
{
    private readonly IPartnerRepository _partnerRepo;
    private readonly ILeadDeduplicationService _dedupService;
    private readonly IKafkaProducer<string, PartnerEvent> _producer;

    public async Task<LeadSubmissionResult> SubmitLeadAsync(
        LeadSubmissionRequest request, Guid partnerId)
    {
        var isDuplicate = await _dedupService.CheckDuplicateAsync(
            request.PANNumber, request.PhoneNumber, partnerId);

        if (isDuplicate)
        {
            return new LeadSubmissionResult
            {
                Success = false,
                Reason = "Duplicate lead — customer already in system"
            };
        }

        var lead = new Lead
        {
            LeadId = Guid.NewGuid(),
            PartnerId = partnerId,
            CustomerName = request.CustomerName,
            PANNumber = request.PANNumber,
            Phone = request.PhoneNumber,
            LoanProduct = request.LoanProduct,
            RequestedAmount = request.LoanAmount,
            Status = "New",
            CreatedAt = DateTime.UtcNow
        };

        await _partnerRepo.SaveLeadAsync(lead);

        await _producer.ProduceAsync("partner-events", new PartnerEvent
        {
            EventType = "LeadSubmitted",
            LeadId = lead.LeadId,
            PartnerId = partnerId,
            LoanProduct = request.LoanProduct,
            Timestamp = DateTime.UtcNow
        });

        return new LeadSubmissionResult
        {
            Success = true,
            LeadId = lead.LeadId,
            EstimatedProcessingTime = GetEstimatedProcessingTime(
                request.LoanProduct)
        };
    }
}

17. Customer App and Self-Service

The customer-facing mobile app and web portal are the primary touchpoints for loan applicants and existing borrowers. The app must provide a seamless experience from application submission through loan closure, with features like real-time application tracking, document upload via camera, EMI payment history, foreclosure calculator, and statement download. The self-service portal reduces call center load and improves customer satisfaction by giving borrowers direct access to their loan information.

Customer App Features

  • Application Tracker: Visual timeline showing application progress through each stage (submitted, KYC verified, under review, approved, disbursed)
  • Document Upload: Camera-based document capture with OCR auto-fill and real-time validation
  • EMI Dashboard: Upcoming EMIs, payment history, next EMI date, total interest paid
  • Repayment Options: Pay via UPI, NEFT, or register NACH mandate
  • Foreclosure Calculator: Calculate foreclosure amount including any penal charges
  • Statements and NOCs: Download loan statements, interest certificates, and No Objection Certificates
  • Customer Support: In-app chatbot, ticket creation, callback scheduling
  • Notifications: Push notifications for EMI reminders, payment confirmations, status updates
public class CustomerDashboardService
{
    private readonly ILoanAccountRepository _accountRepo;
    private readonly IApplicationRepository _appRepo;

    public async Task<CustomerDashboard> GetDashboardAsync(Guid customerId)
    {
        var applications = await _appRepo.GetByCustomerAsync(customerId);
        var activeLoans = await _accountRepo.GetActiveByCustomerAsync(customerId);

        return new CustomerDashboard
        {
            ActiveApplications = applications
                .Where(a => !new[] { "Closed", "Rejected", "Cancelled" }
                    .Contains(a.Status))
                .Select(a => new ApplicationSummary
                {
                    ApplicationId = a.ApplicationId,
                    Product = a.LoanProductType,
                    Amount = a.RequestedAmount,
                    Status = a.Status,
                    SubmittedAt = a.SubmittedAt,
                    StatusDescription = GetStatusDescription(a.Status)
                }).ToList(),

            ActiveLoans = activeLoans.Select(l => new LoanSummary
            {
                AccountNumber = l.AccountNumber,
                Product = l.LoanProductType,
                OutstandingPrincipal = l.OutstandingPrincipal,
                MonthlyEMI = l.MonthlyEMI,
                NextEmiDate = GetNextEmiDate(l),
                InterestRate = l.InterestRate,
                DaysPastDue = l.DaysPastDue,
                TotalPaid = (l.EMIPaid * l.MonthlyEMI),
                ProgressPercentage = (decimal)l.EMIPaid / l.EMITotal * 100
            }).ToList(),

            UpcomingEmis = activeLoans
                .SelectMany(l => l.EMISchedule
                    .Where(e => e.Status == "Pending"))
                .OrderBy(e => e.DueDate)
                .Take(5)
                .Select(e => new EmiDue
                {
                    AccountNumber = e.LoanAccount.AccountNumber,
                    InstallmentNumber = e.InstallmentNumber,
                    DueDate = e.DueDate,
                    Amount = e.EMIAmount,
                    DaysUntilDue = (e.DueDate - DateOnly.FromDateTime(
                        DateTime.UtcNow)).Days
                }).ToList()
        };
    }
}

18. Interest Calculation Engine

Accurate interest calculation is fundamental to a lending platform. Indian lending supports two primary methods: Reducing Balance (also called Diminishing Balance) where interest is calculated on the outstanding principal each month, and Flat Rate where interest is calculated on the original principal for the entire tenure. RBI mandates the use of Annual Percentage Rate (APR) and Effective Interest Rate (EIR) disclosures so customers can compare loans accurately. The interest engine must also handle penal interest for late payments, late payment charges, and GST on processing fees.

Calculation Methods Comparison

MethodFormulaExample (5L, 12%, 24 mo)True Cost
Reducing BalanceInterest on outstanding principal each monthTotal interest: approx. 64,000 INRLower effective rate
Flat RateInterest on original principal x tenureTotal interest: 1,20,000 INRHigher effective rate
Bullet RepaymentInterest accrues, principal + interest at maturityFull amount at endUsed for short-term gold loans
public class InterestCalculationEngine
{
    public List<EmiScheduleItem> CalculateReducingBalanceSchedule(
        decimal principal, decimal annualRate, int tenureMonths)
    {
        var monthlyRate = annualRate / 12 / 100;
        var emi = CalculateEmi(principal, monthlyRate, tenureMonths);
        var schedule = new List<EmiScheduleItem>();
        var outstanding = principal;

        for (int i = 1; i <= tenureMonths; i++)
        {
            var interestComponent = outstanding * monthlyRate;
            var principalComponent = emi - interestComponent;
            outstanding -= principalComponent;

            schedule.Add(new EmiScheduleItem
            {
                InstallmentNumber = i,
                EMIAmount = Math.Round(emi, 2),
                PrincipalComponent = Math.Round(principalComponent, 2),
                InterestComponent = Math.Round(interestComponent, 2),
                OutstandingAfter = Math.Round(Math.Max(outstanding, 0), 2)
            });
        }

        return schedule;
    }

    public List<EmiScheduleItem> CalculateFlatRateSchedule(
        decimal principal, decimal flatAnnualRate, int tenureMonths)
    {
        var totalInterest = principal * flatAnnualRate / 100 * tenureMonths / 12;
        var totalPayable = principal + totalInterest;
        var emi = totalPayable / tenureMonths;
        var principalPerMonth = principal / tenureMonths;

        return Enumerable.Range(1, tenureMonths).Select(i => new EmiScheduleItem
        {
            InstallmentNumber = i,
            EMIAmount = Math.Round(emi, 2),
            PrincipalComponent = Math.Round(principalPerMonth, 2),
            InterestComponent = Math.Round(emi - principalPerMonth, 2),
            OutstandingAfter = Math.Round(
                principal - (principalPerMonth * i), 2)
        }).ToList();
    }

    private decimal CalculateEmi(decimal principal, decimal monthlyRate, int months)
    {
        if (monthlyRate == 0) return principal / months;
        return principal * monthlyRate *
            (decimal)Math.Pow((double)(1 + monthlyRate), months) /
            ((decimal)Math.Pow((double)(1 + monthlyRate), months) - 1);
    }

    public decimal CalculatePenalInterest(
        decimal outstandingPrincipal, decimal penalRate, int overdueDays)
    {
        return outstandingPrincipal * penalRate / 100 / 365 * overdueDays;
    }

    public ForeclosureQuote CalculateForeclosureAmount(
        LoanAccount account, DateOnly foreclosureDate)
    {
        var outstandingPrincipal = account.OutstandingPrincipal;
        var accruedInterest = CalculateAccruedInterest(account, foreclosureDate);
        var penalCharges = CalculateTotalPenalCharges(account, foreclosureDate);
        var foreclosureFee = account.PrincipalAmount * 0.02m;

        return new ForeclosureQuote
        {
            OutstandingPrincipal = outstandingPrincipal,
            AccruedInterest = accruedInterest,
            PenalCharges = penalCharges,
            ForeclosureFee = foreclosureFee,
            GSTonFees = (foreclosureFee + penalCharges) * 0.18m,
            TotalPayable = outstandingPrincipal + accruedInterest +
                penalCharges + foreclosureFee +
                (foreclosureFee + penalCharges) * 0.18m,
            ValidTill = DateTime.UtcNow.AddDays(3)
        };
    }
}

19. Foreclosure and Prepayment

Foreclosure and prepayment allow borrowers to repay their loans ahead of schedule. Full foreclosure closes the loan entirely, while part-prepayment reduces the outstanding principal. RBI regulations protect borrowers by limiting foreclosure penalties: for floating-rate home loans, there should be no foreclosure charges per RBI guidelines; for personal loans and gold loans, lenders may charge up to 3 to 5 percent of the outstanding principal. When a part-prepayment is made, the customer is given the option to either reduce the EMI amount while keeping the same tenure, or reduce the tenure while keeping the same EMI amount.

Prepayment vs Foreclosure Rules

Loan TypePrepayment AllowedPart-PrepaymentFull ForeclosurePenalty
Personal Loan (Fixed)YesYesYes2 to 5% of outstanding
Personal Loan (Floating)YesYesYes0 to 2% of outstanding
Gold LoanYesYesYesNil to 1%
Home Loan (Fixed)YesYesYes2 to 3% of outstanding
Home Loan (Floating)Yes (no penalty per RBI)YesYesNil
Business LoanYesYesYes1 to 4% of outstanding
public class ForeclosureService
{
    private readonly ILoanAccountRepository _accountRepo;
    private readonly IInterestCalculationEngine _interestEngine;
    private readonly IPaymentGateway _paymentGateway;
    private readonly IKafkaProducer<string, ForeclosureEvent> _producer;

    public async Task<ForeclosureResult> ProcessForeclosureAsync(
        Guid accountId, ForeclosureType type, decimal? partAmount = null)
    {
        var account = await _accountRepo.GetWithScheduleAsync(accountId);
        var foreclosureDate = DateOnly.FromDateTime(DateTime.UtcNow);

        var quote = _interestEngine.CalculateForeclosureAmount(
            account, foreclosureDate);

        if (type == ForeclosureType.PartPrepayment && partAmount.HasValue)
        {
            var newPrincipal = account.OutstandingPrincipal - partAmount.Value;
            var newSchedule = _interestEngine.CalculateReducingBalanceSchedule(
                newPrincipal, account.InterestRate,
                account.TenureMonths - account.EMIPaid);

            return new ForeclosureResult
            {
                Quote = quote,
                PartPrepaymentAmount = partAmount.Value,
                NewEMI = newSchedule.First().EMIAmount,
                NewTenureMonths = newSchedule.Count,
                OptionRecommended = "Reduce Tenure",
                PayableNow = partAmount.Value +
                    (partAmount.Value *
                        CalculatePrepaymentPenaltyRate(account) / 100)
            };
        }

        return new ForeclosureResult
        {
            Quote = quote,
            Type = ForeclosureType.FullForeclosure,
            PayableNow = quote.TotalPayable
        };
    }

    public async Task<ForeclosureResult> ExecuteForeclosureAsync(
        Guid accountId, string paymentMethod)
    {
        var account = await _accountRepo.GetByIdAsync(accountId);
        var quote = _interestEngine.CalculateForeclosureAmount(
            account, DateOnly.FromDateTime(DateTime.UtcNow));

        var payment = await _paymentGateway.TransferAsync(new PaymentRequest
        {
            Amount = quote.TotalPayable,
            DestinationAccount = "LenderSettlementAccount",
            Reference = $"FORECLOSURE-{account.AccountNumber}",
            Method = paymentMethod
        });

        if (payment.Success)
        {
            account.AccountStatus = "Foreclosed";
            account.OutstandingPrincipal = 0;
            await _accountRepo.UpdateAsync(account);

            foreach (var emi in account.EMISchedule
                .Where(e => e.Status == "Pending"))
            {
                emi.Status = "Waived";
                emi.AmountPaid = 0;
            }

            var certificate = new ForeclosureCertificate
            {
                CertificateNumber = $"FC-{DateTime.UtcNow:yyyyMMdd}-{account.AccountNumber}",
                AccountNumber = account.AccountNumber,
                CustomerName = account.Applicant.FullName,
                ForeclosureDate = DateTime.UtcNow,
                ClosureAmount = quote.TotalPayable,
                LenderSeal = true
            };

            await _producer.ProduceAsync("foreclosure-events",
                new ForeclosureEvent
                {
                    EventType = "LoanForeclosed",
                    AccountId = accountId,
                    ClosureAmount = quote.TotalPayable,
                    Timestamp = DateTime.UtcNow
                });

            return new ForeclosureResult
            {
                Success = true,
                Certificate = certificate,
                RefundAmount = CalculateRefundForExcessPaid(account)
            };
        }

        return new ForeclosureResult { Success = false };
    }
}

20. Regulatory Compliance (RBI / NBFC)

Regulatory compliance is non-negotiable for lending platforms operating in India. The RBI has issued comprehensive guidelines for NBFCs and banks, including the Digital Lending Guidelines (September 2022), Fair Practices Code, KYC Master Direction, and NPA classification norms. Non-compliance can result in penalties, restrictions on lending activities, or even license cancellation. The platform must have built-in compliance checks, automated reporting, and audit trails that can withstand regulatory scrutiny.

Key RBI Regulatory Requirements

RegulationRequirementPlatform Implementation
Digital Lending GuidelinesLoan disbursed only to borrower bank accountDisbursement API validates destination account
Digital Lending GuidelinesGrievance redressal officer mandatoryCustomer portal with escalation matrix
Fair Practices CodeStandardized loan agreement formatTemplate engine with version control
KYC Master DirectionCDD for all customers, EDD for high-riskKYC service with Aadhaar, PAN, video KYC
NPA Classification90+ DPD equals NPA, provisioning normsDaily DPD monitoring and auto-classification
Data LocalizationAll customer data stored in IndiaIndia-region cloud deployment only
Interest Rate DisplayAPR, EIR, total cost displayed upfrontLoan calculator with full cost disclosure
Loan RecoveryNo unfair practices in collectionCollection scripts with compliance checks
CRILC ReportingMonthly report of top borrowersAutomated report generation and submission
GST ComplianceGST on processing fees and penaltiesAutomatic GST calculation on all charges
public class RegulatoryComplianceService
{
    private readonly ILoanAccountRepository _accountRepo;
    private readonly IReportingRepository _reportRepo;
    private readonly IRbiReportingClient _rbiClient;

    public async Task<CrilcReport> GenerateCrilcReportAsync(
        DateOnly reportingDate)
    {
        var loans = await _accountRepo.GetActiveLoansAsOfAsync(reportingDate);

        var report = new CrilcReport
        {
            ReportingDate = reportingDate,
            NBFCName = "DigitalLending Corp NBFC",
            RBIRegistrationNumber = "N-12345",
            Borrowers = loans.Select(l => new CrilcBorrower
            {
                BorrowerName = l.Applicant.FullName,
                PAN = l.Applicant.PANNumber,
                AccountNumber = l.AccountNumber,
                SanctionedAmount = l.PrincipalAmount,
                OutStandingAmount = l.OutstandingPrincipal,
                Classification = l.AccountStatus,
                DPD = l.DaysPastDue,
                DateOfDisbursal = l.DisbursalDate,
                DateOfLastPayment = l.Transactions
                    .Where(t => t.TransactionType == "EMI_Payment")
                    .Max(t => (DateOnly?)t.CompletedAt)
                    ?.ToUniversalTime()
            }).ToList()
        };

        return report;
    }

    public async Task<ComplianceCheckResult> RunFairPracticesCheckAsync(
        LoanApplication application)
    {
        var violations = new List<ComplianceViolation>();

        if (!application.AprDisclosureProvided)
            violations.Add(new ComplianceViolation
            {
                Code = "FPC_001",
                Severity = "High",
                Description = "APR not disclosed to customer before sanction"
            });

        if (application.ProcessingFee == null ||
            application.ProcessingFee == 0 &&
            application.ActualProcessingFeeCharged > 0)
            violations.Add(new ComplianceViolation
            {
                Code = "FPC_002",
                Severity = "Critical",
                Description = "Processing fee not disclosed upfront"
            });

        if (!application.CoolingOffPeriodCommunicated)
            violations.Add(new ComplianceViolation
            {
                Code = "FPC_003",
                Severity = "Medium",
                Description = "Cooling-off period not communicated"
            });

        return new ComplianceCheckResult
        {
            ApplicationId = application.ApplicationId,
            IsCompliant = !violations.Any(),
            Violations = violations,
            CheckedAt = DateTime.UtcNow
        };
    }
}

21. Fraud Detection

Fraud detection in digital lending addresses three primary fraud types: application fraud involving identity theft or falsified information, income fraud involving inflated income to qualify for larger loans, and document fraud involving forged salary slips, bank statements, or property documents. The platform must employ multiple layers of fraud detection, from real-time rule-based checks during application to post-disbursement pattern analysis for early warning signals.

Fraud Detection Layers

LayerDetection MethodTimingTarget Fraud Type
Identity VerificationAadhaar biometric match, video KYCDuring applicationIdentity theft
Document ForensicsOCR cross-validation, metadata analysisDuring document uploadDocument forgery
Income VerificationBank statement analysis, employer verificationDuring underwritingIncome inflation
Duplicate DetectionPAN, phone, email, address dedupDuring applicationMultiple applications
Device FingerprintingDevice ID, IP geolocation, behavior analysisReal-timeBot or fraud ring activity
Network AnalysisGraph analysis of connections between applicantsBatch processingFraud rings
Post-DisbursementRepayment behavior, account monitoringOngoingStrategic default
public class FraudDetectionService
{
    private readonly IFraudRuleEngine _ruleEngine;
    private readonly IDocumentForensicsService _forensics;
    private readonly IDeviceFingerprintService _deviceService;
    private readonly IGraphAnalysisService _graphService;
    private readonly IKafkaProducer<string, FraudEvent> _producer;

    public async Task<FraudAssessment> AssessApplicationAsync(
        LoanApplication application, DeviceInfo deviceInfo)
    {
        var signals = new List<FraudSignal>();

        var deviceRisk = await _deviceService.AnalyzeAsync(deviceInfo);
        signals.Add(new FraudSignal
        {
            Signal = "DeviceRisk",
            Score = deviceRisk.RiskScore,
            Details = $"Known fraud device: {deviceRisk.IsKnownFraudDevice}, " +
                      $"Multiple applications: {deviceRisk.ApplicationCount}"
        });

        foreach (var doc in application.Documents)
        {
            var forensic = await _forensics.AnalyzeAsync(doc);
            if (forensic.TamperingDetected)
            {
                signals.Add(new FraudSignal
                {
                    Signal = "DocumentTampering",
                    Score = 0.9m,
                    Details = $"Document {doc.Type}: {forensic.TamperingIndicators}"
                });
            }
        }

        var duplicates = await CheckDuplicateApplicationAsync(
            application.Applicant.PANNumber,
            application.Applicant.Phone);
        if (duplicates.Any())
        {
            signals.Add(new FraudSignal
            {
                Signal = "DuplicateApplication",
                Score = 0.85m,
                Details = $"Found {duplicates.Count} similar applications"
            });
        }

        var networkRisk = await _graphService
            .AnalyzeApplicantNetworkAsync(application.ApplicantId);
        if (networkRisk.FraudRingProbability > 0.7m)
        {
            signals.Add(new FraudSignal
            {
                Signal = "FraudRingRisk",
                Score = networkRisk.FraudRingProbability,
                Details = $"Connected to {networkRisk.SuspiciousConnections} flagged accounts"
            });
        }

        var compositeScore = signals.Any() ? signals.Average(s => s.Score) : 0;

        var assessment = new FraudAssessment
        {
            ApplicationId = application.ApplicationId,
            CompositeRiskScore = compositeScore,
            Signals = signals,
            Recommendation = compositeScore > 0.7m ? "ManualReview" :
                             compositeScore > 0.5m ? "EnhancedDueDiligence" :
                             "Pass",
            AssessedAt = DateTime.UtcNow
        };

        if (compositeScore > 0.5m)
        {
            await _producer.ProduceAsync("fraud-events", new FraudEvent
            {
                EventType = "HighRiskApplication",
                ApplicationId = application.ApplicationId,
                RiskScore = compositeScore,
                Signals = signals,
                Timestamp = DateTime.UtcNow
            });
        }

        return assessment;
    }
}

22. Analytics and Reporting

Analytics and reporting serve two audiences: internal business teams who need operational dashboards and regulatory authorities who require standardized reports. The analytics layer must provide real-time visibility into loan origination funnel metrics, portfolio quality indicators, and financial performance metrics. Regulatory reports like CRILC, NPA classification reports, and fair practices compliance reports must be generated monthly and submitted to RBI within prescribed timelines.

Key Portfolio Metrics

MetricFormulaHealthy RangeAlert Threshold
NPA RatioNPA Assets / Total Assets x 100Below 3%Above 5%
Provision Coverage RatioProvisions / NPA Assets x 100Above 70%Below 50%
Collection Efficiency IndexTotal Collections / (Opening Outstanding + Disbursals) x 100Above 95%Below 90%
Average Days to DisburseAvg(Application to Disbursement)Below 48 hoursAbove 72 hours
Approval RateApproved / Total Applications x 10040 to 60%Below 20% or Above 80%
Average Ticket SizeTotal Disbursed / Number of LoansProduct dependentDeviation above 30%
Cost of FundsInterest Expense / Average Borrowings7 to 9%Above 10%
Net Interest Margin(Interest Income - Interest Expense) / Average Assets3 to 5%Below 2%
public class AnalyticsReportingService
{
    private readonly ILoanAccountRepository _accountRepo;
    private readonly IClickHouseClient _clickhouse;
    private readonly IRedisCache _cache;

    public async Task<PortfolioDashboard> GetPortfolioDashboardAsync(
        DateOnly asOfDate)
    {
        var cacheKey = $"portfolio_dashboard:{asOfDate:yyyyMMdd}";
        var cached = await _cache.GetAsync<PortfolioDashboard>(cacheKey);
        if (cached != null) return cached;

        var loans = await _accountRepo.GetAllAsOfAsync(asOfDate);

        var dashboard = new PortfolioDashboard
        {
            AsOfDate = asOfDate,
            TotalActiveLoans = loans.Count(l => l.AccountStatus == "Active"),
            TotalDisbursed = loans.Sum(l => l.PrincipalAmount),
            TotalOutstanding = loans.Sum(l => l.OutstandingPrincipal),
            NpaCount = loans.Count(l => l.AccountStatus == "NPA"),
            NpaRatio = CalculateNpaRatio(loans),
            CollectionEfficiency = await CalculateCollectionEfficiencyAsync(asOfDate),
            AverageTicketSize = loans.Any() ? loans.Average(l => l.PrincipalAmount) : 0,
            ProductBreakdown = loans.GroupBy(l => l.LoanProductType)
                .ToDictionary(g => g.Key, g => new ProductMetrics
                {
                    Count = g.Count(),
                    TotalDisbursed = g.Sum(l => l.PrincipalAmount),
                    NpaRatio = CalculateNpaRatio(g.ToList()),
                    AvgInterestRate = g.Average(l => l.InterestRate)
                }),
            VintageAnalysis = CalculateVintageAnalysis(loans),
            CollectionTrend = await GetCollectionTrendAsync(
                asOfDate.AddMonths(-12), asOfDate)
        };

        await _cache.SetAsync(cacheKey, dashboard, TimeSpan.FromHours(1));
        return dashboard;
    }

    public async Task<List<DailyDisbursementTrend>> GetDisbursementTrendAsync(
        DateOnly from, DateOnly to)
    {
        var query = $@"
            SELECT
                toDate(disbursal_date) as day,
                loan_product_type,
                COUNT(*) as loan_count,
                SUM(principal_amount) as total_disbursed,
                AVG(interest_rate) as avg_rate,
                AVG(tenure_months) as avg_tenure
            FROM loan_accounts
            WHERE disbursal_date BETWEEN '{from:yyyy-MM-dd}' AND '{to:yyyy-MM-dd}'
            GROUP BY day, loan_product_type
            ORDER BY day ASC";

        return await _clickhouse.QueryAsync<DailyDisbursementTrend>(query);
    }
}

23. Cost Estimation

Running a digital lending platform involves significant infrastructure costs, third-party API costs, and compliance overhead. The cost model must account for cloud infrastructure, credit bureau API charges per fetch, KYC verification costs, payment gateway fees, SMS and notification costs, compliance tool licenses, and human resources for operations and collections.

Monthly Cost Breakdown (10,000 applications per month)

CategoryItemMonthly Cost (INR)Per-Application Cost
Cloud InfrastructureCompute (3 environments)3,00,00030 INR
Cloud InfrastructureDatabase (PostgreSQL + replicas)1,50,00015 INR
Cloud InfrastructureStorage (documents + backups)50,0005 INR
Cloud InfrastructureKafka cluster + Redis80,0008 INR
Third-Party APIsCredit Bureau (CIBIL + Experian)2,00,00020 INR
Third-Party APIsKYC (Aadhaar + PAN)1,00,00010 INR
Third-Party APIsSMS and Email notifications30,0003 INR
Payment GatewayNEFT, RTGS, IMPS charges50,0005 INR
ComplianceSecurity audit tools, WAF40,0004 INR
Human ResourcesDev team (10 engineers)15,00,000150 INR
Human ResourcesCredit analysts (5)5,00,00050 INR
Human ResourcesOperations team (8)3,20,00032 INR
Total29,70,000297 INR

The per-application cost of approximately 297 INR translates to roughly 36 INR per loan per month for an average 24-month tenure. For a personal loan of 5,00,000 INR at 14 percent interest, the total interest income over 24 months is approximately 75,000 INR, making the infrastructure cost about 5 percent of revenue. This is a healthy ratio, but the cost model must scale carefully as loan volumes grow. Automation and straight-through processing are key to maintaining unit economics.

Cost Optimization Strategies

  • Cache credit bureau responses: Bureau reports are valid for 30 days; caching eliminates redundant API calls
  • Increase STP rate: Every percentage point increase in straight-through processing reduces analyst headcount by approximately 2 percent
  • Use spot instances: Non-critical batch processing can run on spot instances for 60 to 70 percent savings
  • Right-size databases: Move historical data to cold storage; use read replicas only during peak hours
  • Negotiate volume discounts: Credit bureau and payment gateway costs decrease significantly at higher volumes

24. Testing Strategy

Testing a digital lending platform requires covering functional correctness, regulatory compliance, financial accuracy, and integration reliability. The testing pyramid starts with unit tests for individual services, integration tests for API contracts and database interactions, end-to-end tests for complete loan lifecycle workflows, and chaos engineering tests for resilience. Special attention must be paid to interest calculation accuracy because a bug in the interest engine can cause financial losses and regulatory violations.

Test Categories

CategoryCoverage TargetToolsFocus Areas
Unit Tests90%+xUnit, NUnit, MoqInterest calculation, EMI scheduling, rules engine
Integration Tests80%+Testcontainers, WireMockAPI contracts, database operations, Kafka events
End-to-End TestsCritical pathsPlaywright, REST-assuredFull loan lifecycle, payment flows
Contract TestsAll external APIsPactCredit bureau, KYC, payment gateway
Financial Accuracy100%Custom test suiteEMI amounts, foreclosure calculations, interest accrual
Chaos TestsMonthlyChaos Monkey, GremlinDatabase failover, Kafka partition, API timeouts
public class InterestCalculationTests
{
    [Theory]
    [InlineData(500000, 12, 24, 23534)]
    [InlineData(1000000, 10.5, 36, 32514)]
    [InlineData(200000, 15, 12, 18053)]
    public void ReducingBalanceEmi_CalculatesCorrectly(
        decimal principal, decimal annualRate,
        int months, decimal expectedEmi)
    {
        var engine = new InterestCalculationEngine();
        var schedule = engine.CalculateReducingBalanceSchedule(
            principal, annualRate, months);

        Assert.Equal(expectedEmi, schedule.First().EMIAmount, 0);

        var totalPaid = schedule.Sum(s => s.EMIAmount);
        var totalInterest = schedule.Sum(s => s.InterestComponent);
        Assert.Equal(principal + totalInterest, totalPaid, 0);

        Assert.Equal(0, schedule.Last().OutstandingAfter, 2);

        foreach (var item in schedule)
        {
            Assert.Equal(item.EMIAmount,
                item.PrincipalComponent + item.InterestComponent, 2);
        }
    }

    [Fact]
    public void Foreclosure_CalculatesCorrectPayoff()
    {
        var account = CreateTestLoanAccount(
            principal: 500000, rate: 12, tenure: 24, emisPaid: 6);
        var engine = new InterestCalculationEngine();
        var quote = engine.CalculateForeclosureAmount(
            account, DateOnly.FromDateTime(DateTime.UtcNow));

        Assert.True(quote.OutstandingPrincipal < 500000);
        Assert.True(quote.TotalPayable > quote.OutstandingPrincipal);
        Assert.Equal(
            quote.OutstandingPrincipal * 0.02m,
            quote.ForeclosureFee, 2);
    }

    [Fact]
    public void FlatRateSchedule_Vs_ReducingBalance_FlatRateHigher()
    {
        var engine = new InterestCalculationEngine();
        var reducing = engine.CalculateReducingBalanceSchedule(500000, 12, 24);
        var flat = engine.CalculateFlatRateSchedule(500000, 12, 24);

        var reducingTotalInterest = reducing.Sum(s => s.InterestComponent);
        var flatTotalInterest = flat.Sum(s => s.InterestComponent);
        Assert.True(flatTotalInterest > reducingTotalInterest);
    }
}

25. Interview Q&A

Q1: How would you handle a scenario where the credit bureau API is down during a loan application?

A: The system should implement a circuit breaker pattern around the credit bureau integration. If the primary bureau (CIBIL) is unavailable, the system should automatically failover to the secondary bureau (Experian). If all bureaus are unavailable, the application should be queued for asynchronous processing — the customer receives an acknowledgment that their application is being processed, and the system retries bureau fetch with exponential backoff. For time-sensitive products like gold loans where instant disbursement is expected, a degraded mode could use cached credit scores if available and within 30-day validity while flagging the application for post-disbursement verification. The SLA for credit bureau availability should be monitored with PagerDuty alerts, and the team should maintain relationships with at least three bureau providers to ensure redundancy.

Q2: How do you ensure that EMI calculations are accurate across millions of accounts?

A: The interest calculation engine is implemented as a pure function with deterministic outputs — given the same inputs (principal, rate, tenure), it always produces the same EMI amount. We use decimal arithmetic throughout, never floating point, to avoid rounding errors. The engine has 100 percent unit test coverage with test cases covering edge cases like zero interest, single-month tenure, very large amounts, and partial paise rounding. Additionally, we run a reconciliation job nightly that recalculates all active EMI schedules and compares them against stored values. Any discrepancy triggers an alert and automatic account lock to prevent further transactions until the discrepancy is resolved. The reconciliation itself is triple-checked against an independent Excel-based calculator maintained by the finance team.

Q3: How would you design the system to handle 1 lakh loan applications per day during a festive season offer?

A: The platform must be horizontally scalable at every layer. The API Gateway uses auto-scaling groups to handle traffic spikes. The Loan Origination Service is stateless and can scale to 50 or more instances. The database uses read replicas and connection pooling. Credit bureau API calls are rate-limited by the bureau, so we implement a request queue with prioritization. The key bottleneck is typically the credit bureau API which is rate-limited to approximately 100 QPS. To handle this, we pre-fetch credit scores for pre-approved customer segments hours before the offer goes live, caching results in Redis. This reduces real-time bureau calls by 60 to 70 percent. We also implement a virtual waiting room pattern where customers are queued during peak traffic and given staggered time slots for application submission.

Q4: Explain how you would implement the NPA classification and provisioning logic.

A: NPA classification follows RBI norms: an asset becomes NPA when interest or principal payment is overdue for more than 90 days. The system runs a daily batch job that calculates DPD for every loan account by finding the oldest overdue EMI. When DPD crosses 90, the account status changes to NPA and provisioning begins. For sub-standard assets (NPA less than 12 months), provisioning is 15 percent; for doubtful assets (12 to 36 months), it ranges from 25 to 40 percent; and for loss assets (36 or more months), 100 percent provisioning applies. The provisioning calculation is stored as double-entry accounting entries in the general ledger, with automated reconciliation against the loan portfolio. The entire process is audited monthly by internal audit and quarterly by external auditors as required by RBI.

Q5: How do you prevent and detect loan stacking where a borrower takes multiple loans simultaneously?

A: Loan stacking is detected at multiple levels. During application, the system checks the borrower PAN against existing loans in the platform. Simultaneously, we query the credit bureau for recent inquiries — if another lender has pulled the borrower credit report in the last 48 hours, we flag it for additional verification. We also integrate with the Account Aggregator framework to check the borrower actual bank statement for EMIs being debited to other lenders. Post-disbursement, we monitor the borrower credit report monthly via scheduled bureau pulls for new loans at other institutions. If new loans are detected, we increase the risk score and trigger a portfolio review.

Q6: How would you handle loan restructuring for borrowers facing financial difficulty?

A: Loan restructuring follows RBI Resolution Framework. When a borrower requests restructuring, the system creates a restructuring proposal with options: tenure extension (reducing EMI), interest rate reduction, moratorium period, or a combination. The restructuring engine recalculates the entire EMI schedule based on the chosen option and generates a restructuring agreement for digital signature. The account status is temporarily changed to Restructured and the NPA clock is reset per RBI guidelines. We track restructured accounts separately for reporting purposes and apply a higher provisioning rate (10 percent for first restructuring, 15 percent for subsequent).

Q7: Describe the event-driven architecture for the complete loan lifecycle.

A: The loan lifecycle is modeled as a series of domain events published to Kafka topics. Key events include: ApplicationSubmitted, KycVerified, CreditReportFetched, UnderwritingCompleted, LoanApproved, LoanBooked, DisbursementInitiated, DisbursementCompleted, EmiDue, EmiPaid, EmiBounced, DpdBreached, NpaClassified, ForeclosureRequested, ForeclosureCompleted, LoanClosed. Each event triggers downstream processing — for example, LoanBooked triggers mandate registration, EmiBounced triggers dunning, NpaClassified triggers provisioning calculation. We use event sourcing for the loan account aggregate, storing every state change as an immutable event that can be replayed for audit or debugging. This provides a complete, unalterable history of every action taken on a loan account, which is essential for regulatory compliance and dispute resolution.

Q8: How do you ensure regulatory compliance in a system that evolves rapidly?

A: Regulatory compliance is treated as a first-class concern with dedicated infrastructure. We maintain a regulatory change management process where every RBI circular is analyzed for system impact and tracked as a work item. Compliance rules are implemented in a configurable rules engine, not hardcoded, allowing rapid updates without deployment. We have automated compliance checks that run daily: Fair Practices Code audit (APR disclosure, cooling-off period communication), NPA classification accuracy, data localization verification, and collection practice monitoring. The compliance team has a dashboard showing real-time compliance status with alerts for any violations. Every compliance rule has an owner, an SLA for updates when regulations change, and automated test coverage to prevent regressions.

Q9: How would you design the gold loan product differently from personal loans?

A: Gold loans are fundamentally collateral-based lending, not income-based. The underwriting focuses on the gold value (weight x purity x gold rate), not the borrower creditworthiness. The key differences: (1) LTV ratio is capped at 75% by RBI regardless of borrower profile; (2) Gold appraisal is done at the branch with real-time gold rate feed from MCX/IBJA; (3) Gold is physically stored in the branch vault with CCTV monitoring and insurance; (4) Gold loan tenure is typically shorter (6 to 36 months) with bullet repayment option; (5) Foreclosure is encouraged with minimal or no charges; (6) The credit score requirement is minimal — even first-time borrowers with no credit history can get gold loans. The system architecture reflects these differences: the underwriting engine for gold loans is lightweight and fast, the disbursement is near-instant, and the collection system prioritizes physical gold retrieval over financial recovery in case of default.

Q10: What are the key metrics you would monitor in production for a lending platform?

A: The key metrics are organized into four categories. Business metrics: daily application count, approval rate, average ticket size, disbursement volume, and net interest margin. Operational metrics: application processing time (P50, P95, P99), credit bureau API latency and success rate, KYC verification time, disbursement success rate, and NACH mandate success rate. Risk metrics: NPA ratio by vintage, collection efficiency index, provision coverage ratio, and fraud detection rate. System metrics: API gateway latency and error rate, database connection pool utilization, Kafka consumer lag, cache hit rate, and infrastructure cost per loan. All metrics are tracked in Grafana dashboards with PagerDuty alerts for threshold breaches. The most critical alert is the disbursement success rate — if it drops below 95 percent, it indicates a payment gateway issue that directly impacts customer experience and revenue.

Production Checklist

  • Implement circuit breaker pattern for all external integrations (credit bureau, KYC, payment gateway)
  • Use event sourcing for loan account aggregate to maintain complete audit trail
  • Deploy in India-region cloud only for RBI data localization compliance
  • Cache credit bureau responses for 30 days to reduce API costs and latency
  • Implement triple reconciliation for interest calculations (system, reconciliation job, finance team check)
  • Use Kafka for all async workflows with exactly-once delivery semantics
  • Implement configurable compliance rules engine for rapid regulatory changes
  • Monitor NPA ratio, collection efficiency, and disbursement success rate as top SLOs
  • Maintain at least three credit bureau integrations for redundancy
  • Run daily DPD calculation batch and monthly CRILC report generation
  • Use strategy pattern for loan products to enable adding new products without core changes
  • Implement device fingerprinting and graph analysis for fraud detection

Common Interview Mistakes to Avoid

  • Ignoring regulatory compliance — RBI Digital Lending Guidelines must be discussed from the start
  • Not discussing event sourcing — the audit trail requirement for lending is non-negotiable
  • Using floating point for interest calculations — always use decimal to avoid rounding errors
  • Forgetting about NPA management — it is a significant part of the lending business model
  • Ignoring third-party dependency management — credit bureau and payment gateway failures are common
  • Not discussing gold loan vs personal loan differences — the interview expects product-specific thinking
  • Skipping the fraud detection discussion — identity theft and income fraud are prevalent in digital lending
  • Not mentioning the Account Aggregator framework — it is a key enabler for Indian digital lending

Key Takeaways

A digital lending platform is a complex system that integrates financial domain knowledge with distributed systems engineering. The architecture must balance speed of loan origination with rigor of credit assessment, while maintaining regulatory compliance at every step. The core technical challenges are event-driven workflow orchestration across multiple microservices, accurate financial calculations with decimal precision, real-time integration with external services (credit bureaus, KYC providers, payment gateways), and comprehensive audit trails for regulatory compliance. Mastering this design gives you a deep understanding of both fintech domain modeling and enterprise-grade system architecture, skills that are valuable across the financial services industry.

Whether you are building a personal loan platform, a gold loan system for rural markets, or a home loan origination engine, the fundamental principles remain the same: use event sourcing for audit compliance, implement a configurable rules engine for regulatory changes, integrate with multiple credit bureaus for redundancy, design the collection system for escalation and NPA management, and always use decimal arithmetic for financial calculations. The companies that have mastered these principles — Bajaj Finserv, Lendingkart, KreditBai, and Zest AI — have built lending platforms that serve millions of customers with sub-hour loan disbursement and industry-leading portfolio quality.

Ayodhyya — System Design Blog Series

Digital Lending Platform Design — Senior+ Guide