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
Table of Contents
- Introduction — The Digital Lending Landscape
- The Fintech Lending Revolution
- Functional and Non-Functional Requirements
- Capacity Estimation and Back-of-Envelope
- Data Model and Storage Schema
- High-Level Architecture
- API Design
- Loan Application Flow
- Credit Scoring and Underwriting (CIBIL / Experian)
- KYC and Document Verification
- Loan Approval Workflow
- Disbursement Pipeline
- EMI Collection and Repayment
- NPA Management and Collections
- Loan Product Variants
- Partner and DSA Portal
- Customer App and Self-Service
- Interest Calculation Engine
- Foreclosure and Prepayment
- Regulatory Compliance (RBI / NBFC)
- Fraud Detection
- Analytics and Reporting
- Cost Estimation
- Testing Strategy
- 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.
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.
| Era | Channel | Processing Time | Underwriting | Customer Experience |
|---|---|---|---|---|
| Traditional Banking | Branch visit | 2 to 4 weeks | Manual, relationship-based | Opaque, phone follow-ups |
| Early Digital (2010-2015) | Web portal | 3 to 7 days | Semi-automated rules | Status page, email updates |
| Fintech Lending (2015-2020) | Mobile-first | 1 to 3 days | ML scoring, API-based bureau | Real-time tracking, push notifications |
| Instant Lending (2020-present) | Embedded, BNPL | Minutes to hours | AI-driven, alternative data | Pre-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
| # | Requirement | Priority | Details |
|---|---|---|---|
| F1 | Loan application submission | Must | Customer submits personal, financial, and employment details |
| F2 | KYC verification | Must | Aadhaar eKYC, PAN verification, document upload and OCR |
| F3 | Credit bureau integration | Must | Fetch CIBIL, Experian, CRIF scores and credit reports |
| F4 | Automated credit scoring | Must | ML-based risk assessment with rules engine overlay |
| F5 | Loan approval workflow | Must | Multi-level approval: auto-approve, analyst review, credit committee |
| F6 | Loan disbursement | Must | NEFT, RTGS, IMPS, UPI disbursement with reconciliation |
| F7 | EMI calculation and scheduling | Must | Reduce balance, flat rate, bullet repayment schedules |
| F8 | Repayment collection | Must | NACH auto-debit, UPI mandate, manual payment |
| F9 | Foreclosure and prepayment | Should | Part-prepayment, full foreclosure with penal charge calculation |
| F10 | NPA management | Must | Dunning workflows, collection agent assignment, legal notice generation |
| F11 | Partner and DSA portal | Should | Lead submission, commission tracking, loan status visibility |
| F12 | Customer self-service portal | Should | Loan status, repayment history, NOC download, statement generation |
| F13 | Regulatory reporting | Must | RBI returns, CRILC reporting, GST input credit reconciliation |
| F14 | Fraud detection | Must | Application fraud, identity fraud, income fraud detection |
| F15 | Analytics dashboard | Should | Portfolio quality, disbursement trends, collection efficiency |
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Availability | 99.95% | Financial services require high availability; downtime directly impacts revenue |
| Latency — application submission | Less than 2s P95 | Customer expects instant acknowledgment |
| Latency — credit score fetch | Less than 5s P95 | Bureau API response time; must not block UI |
| Latency — disbursement | Less than 30s | Customer expects quick fund transfer |
| Throughput | 10,000 applications per day | Scalable for mid-size NBFC |
| Data durability | 99.999999% | Financial records cannot be lost |
| Encryption | AES-256 at rest, TLS 1.3 in transit | PCI DSS and RBI data localization requirements |
| Audit trail | Immutable, 7-year retention | Regulatory requirement for all lending operations |
| RTO / RPO | RTO: 15 min, RPO: 0 | Zero data loss with quick recovery for disaster scenarios |
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
| Entity | Record Size | Records per Year | Annual Storage |
|---|---|---|---|
| Loan applications | 50 KB | 3,650,000 | approximately 175 GB |
| KYC documents | 2 MB | 3,650,000 | approximately 7 TB |
| EMI transaction logs | 2 KB | 6,000,000 | approximately 12 GB |
| Audit trail | 500 B | 50,000,000 | approximately 25 GB |
| Credit bureau reports | 100 KB | 3,650,000 | approximately 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 Case | Technology | Rationale |
|---|---|---|
| Loan accounts, EMI schedules | PostgreSQL | ACID transactions, complex joins, JSON support |
| Application state machine | PostgreSQL plus Event Sourcing | Audit trail, temporal queries, regulatory compliance |
| KYC documents, signed agreements | AWS S3 or Azure Blob | Durability, lifecycle policies, compliance |
| Credit bureau responses | PostgreSQL plus Redis cache | Cache bureau responses for 30 days to reduce API costs |
| Transaction logs | PostgreSQL plus Kafka | Real-time processing with durable event log |
| Analytics and reporting | ClickHouse or BigQuery | Columnar storage for fast aggregations |
| Session and rate limiting | Redis | In-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.
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.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /v1/applications | Submit new loan application | Customer JWT |
| GET | /v1/applications/{id} | Get application status and details | Customer JWT |
| PUT | /v1/applications/{id}/documents | Upload KYC documents | Customer JWT |
| POST | /v1/applications/{id}/submit | Submit completed application | Customer JWT |
| GET | /v1/loans/{accountNumber} | Get loan account details | Customer JWT |
| GET | /v1/loans/{accountNumber}/schedule | Get EMI repayment schedule | Customer JWT |
| POST | /v1/loans/{accountNumber}/prepay | Initiate prepayment | Customer JWT |
| POST | /v1/loans/{accountNumber}/foreclose | Initiate foreclosure | Customer JWT |
| GET | /v1/loans/{accountNumber}/statements | Download loan statement | Customer JWT |
| POST | /v1/admin/applications/{id}/approve | Approve application (officer) | Officer JWT |
| POST | /v1/admin/applications/{id}/reject | Reject application (officer) | Officer JWT |
| POST | /v1/disburse/{accountNumber} | Initiate disbursement | Officer JWT |
| GET | /v1/partner/leads | Get partner leads | Partner JWT |
| POST | /v1/partner/leads | Submit new lead | Partner JWT |
| GET | /v1/admin/reports/portfolio | Portfolio quality report | Admin JWT |
| GET | /v1/admin/reports/crilc | CRILC regulatory report | Admin 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.
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.
| Bureau | Score Range | API Latency | Cost per Fetch | Data Richness |
|---|---|---|---|---|
| CIBIL (TransUnion) | 300 to 900 | 2 to 5s | 15 to 25 INR | High — most widely used in India |
| Experian India | 300 to 900 | 2 to 4s | 15 to 20 INR | High — detailed trade line data |
| CRIF High Mark | 300 to 900 | 1 to 3s | 10 to 15 INR | Medium — growing market share |
| Equifax India | 300 to 900 | 2 to 5s | 15 to 20 INR | Medium — 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
| Rule | Personal Loan | Gold Loan | Home Loan | Impact |
|---|---|---|---|---|
| Minimum Credit Score | 650 | 500 | 700 | Hard reject if below |
| Maximum DPD in Last 12 Months | 0 days | 30 days | 0 days | Hard reject if exceeded |
| FOIR (Fixed Obligation to Income Ratio) | Up to 50% | Up to 60% | Up to 50% | Affects approved amount |
| Minimum Monthly Income | 25,000 INR | 15,000 INR | 50,000 INR | Hard reject if below |
| Maximum Loan Amount | 25,00,000 INR | Gold value x 75% | Property value x 80% | Cap on approved amount |
| Minimum Vintage (employment) | 6 months | N/A | 24 months | Hard reject if below |
| Existing NPA Check | No active NPA | No active NPA | No active NPA | Hard 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
- Aadhaar eKYC: Send OTP to Aadhaar-linked mobile number or use biometric authentication. Verify name, date of birth, address, and photo from Aadhaar database.
- 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.
- 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.
- Document OCR: Process uploaded salary slips, bank statements, and property documents using OCR to extract key data points for validation.
- 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
| Tier | Loan Amount | Risk Category | Approval Authority | SLA |
|---|---|---|---|---|
| Auto-Approve | Up to 2,00,000 INR | Low Risk | System (rules engine + ML) | Instant |
| Analyst Review | 2,00,000 to 10,00,000 INR | Medium Risk | Credit Analyst | 4 hours |
| Senior Analyst | 10,00,000 to 25,00,000 INR | Medium Risk | Senior Credit Officer | 8 hours |
| Credit Committee | Above 25,00,000 INR | Any | Credit Committee (3 members) | 24 hours |
| Exception Handling | Any amount | High Risk | Risk Committee | 48 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
| # | Check | Verification | Mandatory |
|---|---|---|---|
| 1 | Loan agreement signed | E-Signature or DSC verification | Yes |
| 2 | KYC fresh | KYC expiry date check | Yes |
| 3 | Credit report fresh | Bureau data within 30 days | Yes |
| 4 | NACH mandate registered | Mandate status equals Active | Yes |
| 5 | Insurance activated | Policy number generated | Conditional |
| 6 | Property valuation (home loan) | Valuation report approved | Home Loan only |
| 7 | Gold appraisal (gold loan) | Appraisal report, gold purity verified | Gold Loan only |
| 8 | Income re-verification | Latest salary slip or ITR | Amount 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
| Method | Settlement Time | Success Rate | Cost | Best For |
|---|---|---|---|---|
| NACH Auto-Debit | T+1 to T+2 | 85 to 90% | 1 to 3 INR per tx | Regular monthly EMIs |
| UPI Autopay | Same day | 90 to 95% | Free or minimal | Tech-savvy customers |
| NEFT or RTGS | T+0 to T+1 | 99% | 2 to 5 INR per tx | Foreclosure, large payments |
| Manual Payment (App) | Instant | 95% | Gateway charges | Self-service customers |
| Cash Collection | T+1 | Variable | Agent commission | Rural 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 Range | Classification | Action | Escalation Level |
|---|---|---|---|
| 1 to 30 days | Standard (Special Mention) | SMS, Email, IVR reminders | Automated dunning |
| 31 to 60 days | Sub-standard | Phone calls by collection agent | Level 1 collection team |
| 61 to 90 days | Doubtful | Field visit plus legal notice | Level 2 collection team |
| 91 to 180 days | NPA | Formal NPA classification, SARFAESI notice | Recovery team plus Legal |
| 181 to 365 days | NPA | One-Time Settlement offers, legal proceedings | Legal team plus Management |
| 365 plus days | Write-off candidate | Write-off recommendation, continued recovery | Risk 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.
| Feature | Personal Loan | Gold Loan | Home Loan |
|---|---|---|---|
| Collateral | Unsecured | Gold ornaments | Property (real estate) |
| Max Amount | 25,00,000 INR | Gold value x 75% | Property value x 80% |
| Tenure | 12 to 60 months | 6 to 36 months | 10 to 30 years |
| Interest Rate | 10.5 to 24% p.a. | 7 to 12% p.a. | 8.5 to 11% p.a. |
| Processing Time | 24 to 72 hours | 1 to 4 hours | 7 to 21 days |
| KYC Requirement | Standard eKYC | Standard eKYC | Enhanced KYC plus property docs |
| Income Proof | Required | Optional (LTV-based) | Required (ITR plus salary slips) |
| Property Assessment | N/A | Gold appraisal | Legal plus technical valuation |
| Insurance | Optional | Optional | Mandatory (property plus life) |
| Foreclosure Penalty | 2 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 Product | Commission Rate | Disbursement Linkage | Payment Cycle |
|---|---|---|---|
| Personal Loan | 1.5 to 3% of disbursed amount | On disbursement | Monthly |
| Gold Loan | 0.5 to 1% of disbursed amount | On disbursement | Monthly |
| Home Loan | 0.5 to 1.5% of disbursed amount | On disbursement | Monthly |
| Business Loan | 2 to 4% of disbursed amount | On disbursement | Monthly |
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
| Method | Formula | Example (5L, 12%, 24 mo) | True Cost |
|---|---|---|---|
| Reducing Balance | Interest on outstanding principal each month | Total interest: approx. 64,000 INR | Lower effective rate |
| Flat Rate | Interest on original principal x tenure | Total interest: 1,20,000 INR | Higher effective rate |
| Bullet Repayment | Interest accrues, principal + interest at maturity | Full amount at end | Used 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 Type | Prepayment Allowed | Part-Prepayment | Full Foreclosure | Penalty |
|---|---|---|---|---|
| Personal Loan (Fixed) | Yes | Yes | Yes | 2 to 5% of outstanding |
| Personal Loan (Floating) | Yes | Yes | Yes | 0 to 2% of outstanding |
| Gold Loan | Yes | Yes | Yes | Nil to 1% |
| Home Loan (Fixed) | Yes | Yes | Yes | 2 to 3% of outstanding |
| Home Loan (Floating) | Yes (no penalty per RBI) | Yes | Yes | Nil |
| Business Loan | Yes | Yes | Yes | 1 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
| Regulation | Requirement | Platform Implementation |
|---|---|---|
| Digital Lending Guidelines | Loan disbursed only to borrower bank account | Disbursement API validates destination account |
| Digital Lending Guidelines | Grievance redressal officer mandatory | Customer portal with escalation matrix |
| Fair Practices Code | Standardized loan agreement format | Template engine with version control |
| KYC Master Direction | CDD for all customers, EDD for high-risk | KYC service with Aadhaar, PAN, video KYC |
| NPA Classification | 90+ DPD equals NPA, provisioning norms | Daily DPD monitoring and auto-classification |
| Data Localization | All customer data stored in India | India-region cloud deployment only |
| Interest Rate Display | APR, EIR, total cost displayed upfront | Loan calculator with full cost disclosure |
| Loan Recovery | No unfair practices in collection | Collection scripts with compliance checks |
| CRILC Reporting | Monthly report of top borrowers | Automated report generation and submission |
| GST Compliance | GST on processing fees and penalties | Automatic 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
| Layer | Detection Method | Timing | Target Fraud Type |
|---|---|---|---|
| Identity Verification | Aadhaar biometric match, video KYC | During application | Identity theft |
| Document Forensics | OCR cross-validation, metadata analysis | During document upload | Document forgery |
| Income Verification | Bank statement analysis, employer verification | During underwriting | Income inflation |
| Duplicate Detection | PAN, phone, email, address dedup | During application | Multiple applications |
| Device Fingerprinting | Device ID, IP geolocation, behavior analysis | Real-time | Bot or fraud ring activity |
| Network Analysis | Graph analysis of connections between applicants | Batch processing | Fraud rings |
| Post-Disbursement | Repayment behavior, account monitoring | Ongoing | Strategic 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
| Metric | Formula | Healthy Range | Alert Threshold |
|---|---|---|---|
| NPA Ratio | NPA Assets / Total Assets x 100 | Below 3% | Above 5% |
| Provision Coverage Ratio | Provisions / NPA Assets x 100 | Above 70% | Below 50% |
| Collection Efficiency Index | Total Collections / (Opening Outstanding + Disbursals) x 100 | Above 95% | Below 90% |
| Average Days to Disburse | Avg(Application to Disbursement) | Below 48 hours | Above 72 hours |
| Approval Rate | Approved / Total Applications x 100 | 40 to 60% | Below 20% or Above 80% |
| Average Ticket Size | Total Disbursed / Number of Loans | Product dependent | Deviation above 30% |
| Cost of Funds | Interest Expense / Average Borrowings | 7 to 9% | Above 10% |
| Net Interest Margin | (Interest Income - Interest Expense) / Average Assets | 3 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)
| Category | Item | Monthly Cost (INR) | Per-Application Cost |
|---|---|---|---|
| Cloud Infrastructure | Compute (3 environments) | 3,00,000 | 30 INR |
| Cloud Infrastructure | Database (PostgreSQL + replicas) | 1,50,000 | 15 INR |
| Cloud Infrastructure | Storage (documents + backups) | 50,000 | 5 INR |
| Cloud Infrastructure | Kafka cluster + Redis | 80,000 | 8 INR |
| Third-Party APIs | Credit Bureau (CIBIL + Experian) | 2,00,000 | 20 INR |
| Third-Party APIs | KYC (Aadhaar + PAN) | 1,00,000 | 10 INR |
| Third-Party APIs | SMS and Email notifications | 30,000 | 3 INR |
| Payment Gateway | NEFT, RTGS, IMPS charges | 50,000 | 5 INR |
| Compliance | Security audit tools, WAF | 40,000 | 4 INR |
| Human Resources | Dev team (10 engineers) | 15,00,000 | 150 INR |
| Human Resources | Credit analysts (5) | 5,00,000 | 50 INR |
| Human Resources | Operations team (8) | 3,20,000 | 32 INR |
| Total | 29,70,000 | 297 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
| Category | Coverage Target | Tools | Focus Areas |
|---|---|---|---|
| Unit Tests | 90%+ | xUnit, NUnit, Moq | Interest calculation, EMI scheduling, rules engine |
| Integration Tests | 80%+ | Testcontainers, WireMock | API contracts, database operations, Kafka events |
| End-to-End Tests | Critical paths | Playwright, REST-assured | Full loan lifecycle, payment flows |
| Contract Tests | All external APIs | Pact | Credit bureau, KYC, payment gateway |
| Financial Accuracy | 100% | Custom test suite | EMI amounts, foreclosure calculations, interest accrual |
| Chaos Tests | Monthly | Chaos Monkey, Gremlin | Database 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.