Design a Peer-to-Peer Lending Platform
Building the modern lending marketplace: borrowers, investors, risk, escrow, compliance, and everything in between
Table of Contents
- Introduction — The P2P Lending Landscape
- P2P Lending Fundamentals
- Functional and Non-Functional Requirements
- Capacity Estimation and Back-of-Envelope
- Data Model and Storage Schema
- High-Level Architecture
- API Design
- Borrower Onboarding and KYC
- Credit Assessment and Risk Grading
- Loan Listing and Auction
- Investor Matching and Allocation
- Escrow and Fund Management
- Repayment Collection
- Default and Recovery
- Auto-Invest Rules Engine
- Secondary Market and Loan Trading
- Investor Dashboard
- Regulatory Compliance — RBI NBFC-P2P
- Fraud Prevention and Detection
- Documentation and E-Sign
- Notifications and Communication
- Analytics and Reporting
- Cost Estimation
- Testing Strategy
- Interview Q&A
1. Introduction — The P2P Lending Landscape
Peer-to-peer lending, commonly abbreviated as P2P lending, is a method of debt financing that allows individuals to borrow and lend money directly to one another without the intermediation of a traditional banking institution. The concept emerged in the early 2000s with platforms like Zopa in the United Kingdom, which launched in 2005, followed by Prosper and LendingClub in the United States. By 2025, the global P2P lending market had exceeded 200 billion dollars in cumulative originations, with platforms operating across more than 70 countries. In India specifically, the Reserve Bank of India introduced comprehensive regulatory guidelines for NBFC-P2P (Non-Banking Financial Company — Peer-to-Peer Lending) entities in 2017, establishing a formal framework for the operation of P2P lending platforms.
The fundamental value proposition of P2P lending is straightforward: it connects borrowers who need capital with investors who seek higher returns than traditional fixed-income instruments, while the platform itself provides the technology infrastructure, risk assessment, and regulatory compliance. For borrowers, P2P lending often offers lower interest rates than banks, faster approval times, and more flexible eligibility criteria. For investors, it offers returns that can range from 10 to 25 percent annually, significantly higher than savings accounts or government bonds. The platform earns its revenue by charging a processing fee to borrowers and a commission to investors on each successful loan disbursement.
Building a P2P lending platform is one of the most complex system design challenges in the fintech domain. Unlike simpler systems like URL shorteners or chat applications, a P2P lending platform must handle financial transactions with strict regulatory compliance, implement sophisticated credit assessment algorithms, manage escrow accounts for fund safety, support complex auction and matching mechanisms, provide real-time dashboards for both borrowers and investors, and ensure absolute data integrity because every single byte of data represents real money. A single bug in the repayment calculation engine could lead to millions of dollars in incorrect interest charges, and a failure in the escrow system could result in investor funds being misappropriated. The stakes are extraordinarily high.
This comprehensive guide walks through the complete design of a P2P lending platform, from understanding the business domain to implementing the core services in C#. We will cover borrower onboarding and KYC verification, credit scoring and risk grading, loan listing and auction mechanisms, investor matching and allocation algorithms, escrow fund management, repayment collection with auto-debit, default management and recovery, auto-invest rule engines, secondary market for loan trading, regulatory compliance with RBI and NBFC-P2P guidelines, fraud prevention, document management with e-signatures, notification systems, analytics and reporting dashboards, cost estimation, and testing strategies. By the end of this guide, you will have a thorough understanding of what it takes to build a production-grade P2P lending platform that can handle thousands of loans and millions of dollars in transactions.
2. P2P Lending Fundamentals
Before diving into the system design, it is essential to understand the core concepts and terminology of P2P lending. A typical P2P lending transaction involves several parties: the borrower who requests a loan, the investors (also called lenders) who fund the loan, the P2P platform that facilitates the transaction, an escrow bank where investor funds are held, and potentially a recovery agent in case of default. The platform acts as an intermediary that does not lend its own money but rather connects borrowers with investors and provides the technology and compliance infrastructure.
When a borrower applies for a loan, the platform performs credit assessment, assigns a risk grade, and lists the loan on the marketplace. Investors then browse available loans and decide how much to invest. Once the loan is fully funded (or partially funded, depending on the platform rules), the funds are transferred from the escrow account to the borrower's bank account. The borrower then repays the loan in monthly installments (EMIs) that include both principal and interest. The platform distributes each EMI to the investors proportionally based on their investment in that loan.
Key Terminology
| Term | Definition | Example |
|---|---|---|
| Principal | The original loan amount borrowed | 1,00,000 INR |
| EMI | Equated Monthly Installment | 8,792 INR for 12 months at 12% |
| Risk Grade | Letter or number grade indicating creditworthiness | A+, A, B+, B, C+ |
| Yield | Annualized return for the investor | 14% per annum |
| Default | Failure to pay EMI for 90+ days | NPA (Non-Performing Asset) |
| Escrow | Held funds managed by the platform until disbursement | Investor wallet balance |
| Platform Fee | Commission charged by the platform | 2% of loan amount from borrower |
| Prepayment | Early repayment of loan before tenure ends | Full early settlement |
| NPV | Net Present Value of future cash flows | Discounted at risk-adjusted rate |
| Charge-off | Loan written off as unrecoverable | After 180 days delinquent |
P2P Lending Lifecycle
Revenue Model
The platform generates revenue through multiple streams. The primary source is the processing fee charged to borrowers, typically 1 to 3 percent of the loan amount, deducted at the time of disbursement. The secondary revenue stream is the investor commission, usually 0.5 to 2 percent of the amount invested, collected monthly as a percentage of the EMI received. Additional revenue can come from late payment fees charged to borrowers, prepayment penalties, premium investor features such as advanced analytics and priority access to high-rated loans, and data services provided to institutional investors. The total cost of funds for a P2P platform typically ranges between 3 to 6 percent of total originations.
3. Functional and Non-Functional Requirements
Functional Requirements
| # | Requirement | Priority | Details |
|---|---|---|---|
| F1 | Borrower registration and KYC | Must | PAN, Aadhaar verification, income proof upload |
| F2 | Loan application and credit scoring | Must | Automated risk grade assignment using bureau and alternative data |
| F3 | Loan listing and auction | Must | Borrower posts loan request, investors bid with interest rate |
| F4 | Investor registration and KYC | Must | PAN verification, bank account linking, risk profiling |
| F5 | Escrow fund management | Must | Segregated investor wallets, fund hold before disbursement |
| F6 | Loan disbursement | Must | NEFT/RTGS transfer to borrower bank account |
| F7 | Repayment collection | Must | NACH auto-debit, UPI mandate, manual payment |
| F8 | Investor payout distribution | Must | Pro-rata distribution of EMI to investors |
| F9 | Auto-invest rules | Should | Rule-based automatic investment based on risk, tenure, rate |
| F10 | Secondary market | Should | Investors can sell loan parts to other investors |
| F11 | Investor dashboard | Must | Portfolio overview, returns, defaults, cash flow projections |
| F12 | Default and recovery | Must | Automated dunning, recovery agent assignment, write-off |
| F13 | Document management | Should | Loan agreements, e-signatures, KYC document storage |
| F14 | Regulatory reporting | Must | RBI reporting, NBFC-P2P compliance, audit trail |
| F15 | Notifications | Must | SMS, email, push notifications for all events |
| F16 | Fraud detection | Must | Identity fraud, income fraud, duplicate detection |
| F17 | Analytics and reporting | Should | Platform metrics, investor reports, regulatory dashboards |
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Availability | 99.95% | Financial platform requires high uptime |
| Latency | P99 < 500ms | API response time for dashboard and lending operations |
| Consistency | Strong consistency | Financial data cannot have eventual consistency for balances |
| Throughput | 10,000 TPS | Peak during batch repayment processing |
| Data Durability | 99.999999% | Financial records must be permanently durable |
| Security | SOC 2 Type II | Compliance for financial data handling |
| Audit Trail | Immutable log | Every mutation logged for regulatory audit |
| Data Encryption | AES-256 at rest, TLS 1.3 in transit | PCI DSS and regulatory requirement |
4. Capacity Estimation and Back-of-Envelope
Understanding the scale of operations is crucial for designing the infrastructure. Let us estimate the capacity requirements for a mid-sized P2P lending platform in India targeting 500,000 registered users with 200,000 active loans at any given time. These numbers represent a realistic scenario for a platform that has been operating for approximately two to three years and has achieved product-market fit. The capacity estimates drive infrastructure decisions from database sizing to API gateway configuration to batch processing window scheduling.
User and Transaction Estimates
| Metric | Daily | Monthly | Annual |
|---|---|---|---|
| New borrower registrations | 500 | 15,000 | 180,000 |
| New investor registrations | 200 | 6,000 | 72,000 |
| Loan applications | 300 | 9,000 | 108,000 |
| Loan disbursements | 150 | 4,500 | 54,000 |
| Active loans | 200,000 | - | - |
| Daily EMI collections | 8,000 | 240,000 | 2,880,000 |
| Investment transactions | 2,000 | 60,000 | 720,000 |
| Dashboard page views | 50,000 | 1,500,000 | 18,000,000 |
Storage Estimates
Each loan record occupies approximately 2 KB including metadata. With 54,000 new loans per year and a 36-month average tenure, the active loan data grows to approximately 200,000 records or 400 MB per year for the core loan table. Repayment records are smaller at 500 bytes each, generating roughly 1.44 million records per year or 720 MB. Investor portfolio data adds another 200 MB per year. KYC documents stored as PDFs average 2 MB each, with 252,000 new users per year requiring 504 GB of document storage annually. Audit logs are the largest contributor at approximately 50 GB per year assuming 100 million log entries. The total first-year data footprint including indexes, backups, and replication is approximately 2 TB, growing to 6 TB by the end of year three.
Bandwidth Estimates
Assuming 50,000 daily page views averaging 500 KB each, the read bandwidth for dashboards is approximately 25 GB per day or 290 KB per second. API calls for loan operations, at 20,000 requests per day averaging 2 KB each, contribute 40 MB per day. The batch repayment processing at 8,000 transactions per day generates approximately 16 MB of data. Total daily bandwidth is approximately 25 GB, which translates to a sustained throughput of roughly 300 KB per second with peaks of 5 MB per second during batch processing windows. Network capacity should be provisioned at 10x the peak to handle traffic spikes during repayment cycles at the start and end of each month.
Compute Estimates
The API layer requires approximately 4 CPU cores and 8 GB RAM per node, with 3 nodes for high availability. The credit scoring service needs GPU instances for the ML model inference, approximately 2 NVIDIA T4 instances running behind a load balancer. The batch repayment processor runs on a single dedicated instance with 16 CPU cores and 32 GB RAM, scheduled during off-peak hours (2 AM to 6 AM IST). The auto-invest engine runs continuously and requires 2 instances with 4 CPU cores and 8 GB RAM each. Redis clusters for caching need 3 nodes with 32 GB RAM each for a total of 96 GB cache capacity. The Elasticsearch cluster for search and audit logs requires 5 nodes with 16 CPU cores, 64 GB RAM, and 500 GB SSD storage each.
5. Data Model and Storage Schema
The data model for a P2P lending platform must capture the complete lifecycle of a loan from application to closure. We use a relational database (PostgreSQL) for transactional data due to the strong consistency requirements, and a separate document store for KYC files and loan agreement PDFs. The schema is designed around the principle of immutable event logging: instead of mutating records in place, we append new records and maintain a current-state view through materialized views or read-optimized tables.
Entity Relationship Overview
Core Tables
public class Borrower
{
public Guid Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string PanNumber { get; set; }
public string AadhaarHash { get; set; }
public decimal AnnualIncome { get; set; }
public string EmploymentType { get; set; }
public string EmployerName { get; set; }
public BorrowerStatus Status { get; set; }
public int CreditScore { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? KycVerifiedAt { get; set; }
public string RiskGrade { get; set; }
}
public class LoanRequest
{
public Guid Id { get; set; }
public Guid BorrowerId { get; set; }
public decimal RequestedAmount { get; set; }
public decimal ApprovedAmount { get; set; }
public int TenureMonths { get; set; }
public decimal MaxInterestRate { get; set; }
public string Purpose { get; set; }
public string RiskGrade { get; set; }
public LoanRequestStatus Status { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ListedAt { get; set; }
public DateTime? FullyFundedAt { get; set; }
public DateTime? DisbursedAt { get; set; }
}
public class Loan
{
public Guid Id { get; set; }
public Guid LoanRequestId { get; set; }
public Guid BorrowerId { get; set; }
public decimal PrincipalAmount { get; set; }
public decimal OutstandingPrincipal { get; set; }
public decimal AnnualInterestRate { get; set; }
public int TenureMonths { get; set; }
public decimal MonthlyEmi { get; set; }
public int PaidEmiCount { get; set; }
public LoanStatus Status { get; set; }
public DateTime DisbursedAt { get; set; }
public DateTime MaturityDate { get; set; }
public int DaysPastDue { get; set; }
public decimal TotalRecoveryAmount { get; set; }
}
public class Investor
{
public Guid Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string PanNumber { get; set; }
public decimal TotalInvested { get; set; }
public decimal AvailableBalance { get; set; }
public decimal TotalReturns { get; set; }
public InvestorStatus Status { get; set; }
public string RiskProfile { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? KycVerifiedAt { get; set; }
}
public class Investment
{
public Guid Id { get; set; }
public Guid InvestorId { get; set; }
public Guid LoanId { get; set; }
public decimal Amount { get; set; }
public decimal InvestedAtRate { get; set; }
public decimal ReturnsReceived { get; set; }
public InvestmentStatus Status { get; set; }
public DateTime CreatedAt { get; set; }
}
public class EmiSchedule
{
public Guid Id { get; set; }
public Guid LoanId { get; set; }
public int EmiNumber { get; set; }
public decimal PrincipalPart { get; set; }
public decimal InterestPart { get; set; }
public decimal TotalAmount { get; set; }
public DateTime DueDate { get; set; }
public DateTime? PaidDate { get; set; }
public decimal? PaidAmount { get; set; }
public EmiStatus Status { get; set; }
}
Supporting Tables
| Table | Purpose | Key Columns |
|---|---|---|
| escrow_transactions | Track all fund movements | transaction_id, loan_id, investor_id, amount, type, status |
| auto_invest_rules | Investor auto-invest preferences | investor_id, min_amount, max_amount, risk_grades, min_rate |
| kyc_documents | KYC document metadata | user_id, document_type, file_path, verified_at, verified_by |
| loan_agreements | Generated loan agreements | loan_id, version, file_path, signed_at, e_sign_id |
| audit_logs | Immutable audit trail | log_id, entity_type, entity_id, action, user_id, timestamp, metadata |
| recovery_records | Default recovery tracking | loan_id, agent_id, amount_recovered, recovery_date, method |
| secondary_market_listings | Loan part listings for sale | listing_id, loan_id, seller_id, units, price, status |
| notification_logs | Notification delivery tracking | notification_id, user_id, channel, template, sent_at, delivered |
| credit_bureau_pulls | Credit bureau data snapshots | borrower_id, bureau, score, pulled_at, raw_data_path |
| platform_config | Runtime configuration | config_key, config_value, updated_at, updated_by |
Database Indexing Strategy
public class DatabaseIndexes
{
public const string LoansByBorrower =
"CREATE INDEX IX_loans_borrower_status ON loans(borrower_id, status);";
public const string InvestmentsByInvestor =
"CREATE INDEX IX_investments_investor_status ON investments(investor_id, status);";
public const string EmiPendingByLoan =
"CREATE INDEX IX_emi_pending ON emi_schedule(loan_id, status, due_date);";
public const string ListedLoansByGrade =
"CREATE INDEX IX_loan_requests_listed ON loan_requests(status, risk_grade, requested_amount);";
public const string DefaultLoans =
"CREATE INDEX IX_loans_dpd ON loans(days_past_due, status);";
public const string EscrowByInvestor =
"CREATE INDEX IX_escrow_investor_status ON escrow_transactions(investor_id, status);";
}
6. High-Level Architecture
The P2P lending platform follows a microservices architecture with clear domain boundaries. Each service owns its data and communicates with other services through well-defined APIs and asynchronous event streams. The architecture prioritizes consistency for financial operations and availability for read-heavy dashboards. The separation of concerns ensures that a failure in the notification service does not affect loan disbursement, and a performance issue in the analytics pipeline does not impact repayment collection.
Service Responsibilities
| Service | Responsibility | Consistency | Database |
|---|---|---|---|
| Borrower Service | Registration, profile, KYC status | Strong | PostgreSQL |
| Investor Service | Registration, portfolio, wallet | Strong | PostgreSQL |
| Loan Service | Application, lifecycle, schedule | Strong | PostgreSQL |
| Credit Service | Bureau pull, scoring, grading | Eventual | PostgreSQL + Cache |
| Escrow Service | Fund hold, release, reconciliation | Strong | PostgreSQL |
| Repayment Service | EMI collection, distribution | Strong | PostgreSQL |
| Auction Service | Loan listing, bidding, matching | Strong | PostgreSQL + Redis |
| KYC Service | Identity verification, document check | Eventual | PostgreSQL + S3 |
| Document Service | Agreement generation, e-sign | Eventual | PostgreSQL + S3 |
| Notification Service | SMS, email, push delivery | Eventual | PostgreSQL |
| Fraud Detection | Rule engine, anomaly detection | Eventual | Elasticsearch |
| Auto-Invest Engine | Automatic investment matching | Eventual | Redis + PostgreSQL |
| Secondary Market | Loan part trading | Strong | PostgreSQL |
Event-Driven Communication
Cross-service communication happens through Kafka topics to ensure loose coupling and reliable delivery. Critical events include LoanDisbursed, EmiReceived, EmiOverdue, LoanDefaulted, and InvestmentPayout. Each event carries the complete context needed by consuming services, ensuring that services can be rebuilt from the event log if needed. All events are persisted for 30 days in Kafka and archived to cold storage for audit purposes. Dead letter queues capture events that fail processing after three retries, and an operations dashboard monitors DLQ depth to ensure no events are permanently lost.
Resilience Patterns
The architecture implements circuit breakers on all external integrations (credit bureau, banking gateway, NACH gateway) using Polly. If a circuit breaker trips, the system queues operations for retry rather than failing the user request immediately. The retry policy uses exponential backoff with jitter, starting at 1 second and capping at 30 seconds. For the escrow service, the circuit breaker has a higher threshold (5 failures vs 3) because financial operations must not be interrupted without careful consideration. Health checks run every 15 seconds and are aggregated into a dashboard that shows the real-time health of every service and its dependencies.
7. API Design
The P2P lending platform exposes a RESTful API with versioning. All endpoints require authentication via JWT tokens with a 15-minute expiry and refresh token rotation. Financial operations require additional authorization checks and are protected with idempotency keys to prevent duplicate transactions. The API follows a consistent response envelope with success, data, error, and requestId fields.
Core API Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/v1/borrowers/register | Register new borrower | Public |
| POST | /api/v1/borrowers/kyc | Submit KYC documents | Bearer |
| GET | /api/v1/borrowers/profile | Get borrower profile | Bearer |
| POST | /api/v1/loans/apply | Apply for a loan | Bearer |
| GET | /api/v1/loans/{id}/schedule | Get EMI schedule | Bearer |
| POST | /api/v1/loans/{id}/prepay | Prepay loan | Bearer |
| GET | /api/v1/marketplace/loans | Browse available loans | Bearer |
| POST | /api/v1/investments | Invest in a loan | Bearer |
| GET | /api/v1/investors/portfolio | Get investment portfolio | Bearer |
| POST | /api/v1/investors/auto-invest | Set auto-invest rules | Bearer |
| POST | /api/v1/escrow/deposit | Deposit funds to wallet | Bearer |
| POST | /api/v1/escrow/withdraw | Withdraw available funds | Bearer |
| POST | /api/v1/repayments/pay | Make manual repayment | Bearer |
| POST | /api/v1/secondary-market/list | List loan part for sale | Bearer |
| POST | /api/v1/secondary-market/buy | Buy listed loan part | Bearer |
API Implementation Example
[ApiController]
[Route("api/v1/[controller]")]
[Authorize]
public class InvestmentsController : ControllerBase
{
private readonly IInvestmentService _investmentService;
private readonly IEscrowService _escrowService;
private readonly ILogger<InvestmentsController> _logger;
public InvestmentsController(
IInvestmentService investmentService,
IEscrowService escrowService,
ILogger<InvestmentsController> logger)
{
_investmentService = investmentService;
_escrowService = escrowService;
_logger = logger;
}
[HttpPost]
[Idempotent]
public async Task<ActionResult<InvestmentResponse>> Invest(
[FromBody] InvestRequest request)
{
var investorId = GetAuthenticatedInvestorId();
var loanRequest = await _investmentService
.GetLoanRequestAsync(request.LoanRequestId);
if (loanRequest == null)
return NotFound(new { error = "Loan request not found" });
if (loanRequest.Status != LoanRequestStatus.Listed)
return BadRequest(new { error = "Loan is not available" });
if (request.Amount < loanRequest.MinInvestmentAmount)
return BadRequest(new { error = "Below minimum investment" });
if (request.Amount > loanRequest.RemainingFundingAmount)
return BadRequest(new { error = "Amount exceeds remaining funding" });
var balance = await _escrowService
.GetAvailableBalanceAsync(investorId);
if (balance < request.Amount)
return BadRequest(new { error = "Insufficient wallet balance" });
var investment = await _investmentService
.PlaceInvestmentAsync(investorId, request.LoanRequestId, request.Amount);
_logger.LogInformation(
"Investment placed: Investor={InvestorId}, Loan={LoanId}, Amount={Amount}",
investorId, request.LoanRequestId, request.Amount);
return Ok(new InvestmentResponse
{
InvestmentId = investment.Id,
LoanRequestId = investment.LoanRequestId,
Amount = investment.Amount,
ExpectedRate = investment.InvestedAtRate,
Status = investment.Status.ToString(),
CreatedAt = investment.CreatedAt
});
}
}
Rate Limiting Configuration
| Endpoint Group | Rate Limit | Window | Scope |
|---|---|---|---|
| Registration | 5 requests | 1 hour | Per IP |
| KYC Submission | 10 requests | 1 day | Per user |
| Loan Application | 3 requests | 1 day | Per user |
| Investment | 100 requests | 1 minute | Per user |
| Dashboard | 300 requests | 1 minute | Per user |
| Escrow Deposit | 10 requests | 1 hour | Per user |
8. Borrower Onboarding and KYC
Know Your Customer (KYC) compliance is a regulatory requirement for all financial platforms in India. The RBI mandates that NBFC-P2P platforms verify the identity and address of all borrowers and investors before allowing them to participate in lending or borrowing. The KYC process must be completed within 30 days of registration, and the platform must maintain KYC records for at least 5 years after the closure of the business relationship. The KYC process must balance regulatory compliance with user experience, as overly complex verification flows lead to high dropout rates during onboarding.
KYC Process Flow
KYC Service Implementation
public class KycVerificationService
{
private readonly IAadhaarService _aadhaarService;
private readonly IPanService _panService;
private readonly IFaceMatchService _faceMatchService;
private readonly IOcrService _ocrService;
private readonly IKycRepository _kycRepository;
public async Task<KycResult> VerifyAsync(KycSubmission submission)
{
var result = new KycResult();
var panData = await _ocrService.ExtractPanAsync(submission.PanImage);
var aadhaarData = await _ocrService.ExtractAadhaarAsync(submission.AadhaarFrontImage);
var panResult = await _panService.VerifyAsync(
panData.PanNumber, submission.FullName, panData.DateOfBirth);
result.PanVerified = panResult.IsValid;
result.PanName = panData.NameOnCard;
var aadhaarResult = await _aadhaarService.VerifyAsync(
aadhaarData.UidNumber, submission.FullName);
result.AadhaarVerified = aadhaarResult.IsValid;
var faceResult = await _faceMatchService.CompareAsync(
submission.SelfieImage, aadhaarData.Photo);
result.FaceMatchScore = faceResult.Confidence;
result.FaceMatchPassed = faceResult.Confidence > 0.85m;
var isDuplicate = await _kycRepository
.CheckDuplicateAsync(panData.PanNumber, aadhaarData.UidNumber);
result.IsDuplicate = isDuplicate;
if (result.PanVerified && result.AadhaarVerified && result.FaceMatchPassed && !result.IsDuplicate)
{
result.Status = KycStatus.Approved;
}
else if (result.PanVerified && result.AadhaarVerified && !result.FaceMatchPassed)
{
result.Status = KycStatus.ManualReview;
result.ReviewReason = "Face match below threshold";
}
else
{
result.Status = KycStatus.Rejected;
}
await _kycRepository.SaveResultAsync(submission.UserId, result);
return result;
}
}
KYC Integration Partners
| Verification | Provider | Latency | Cost per Check |
|---|---|---|---|
| Aadhaar eKYC | UIDAI via NSDL | 1-3 seconds | 15 INR |
| PAN Verification | NSDL / Protean | 2-5 seconds | 10 INR |
| Bank Account Verification | Decentro / Setu | 1-2 seconds | 5 INR |
| Face Match | Amazon Rekognition | 1-2 seconds | 2 INR |
| Address Verification | Digilocker API | 2-4 seconds | 8 INR |
| Income Verification | Bank Statement Analyzer | 3-5 seconds | 20 INR |
9. Credit Assessment and Risk Grading
Credit assessment is the heart of a P2P lending platform. The platform must evaluate each borrower's creditworthiness accurately because the risk is borne entirely by the investors, not the platform. A poor credit assessment leads to high default rates, which drives investors away from the platform. The credit assessment process combines traditional credit bureau data with alternative data sources and machine learning models to produce a risk grade for each borrower.
Credit Scoring Model
The credit scoring model takes inputs from multiple sources and produces a score between 300 and 900. The key features used in the model include credit bureau score (weight 30%), debt-to-income ratio (weight 20%), employment stability (weight 15%), credit history length (weight 10%), utilization ratio (weight 10%), recent credit inquiries (weight 8%), and alternative data such as mobile phone usage patterns, utility payment history, and bank transaction analysis (weight 7%). The model is a gradient-boosted decision tree trained on historical loan performance data, retrained quarterly with the latest default data to maintain predictive accuracy.
Risk Grade Mapping
| Score Range | Grade | Description | Expected Default Rate | Interest Rate Range |
|---|---|---|---|---|
| 750-900 | A+ | Excellent creditworthiness | < 1% | 10-12% |
| 700-749 | A | Very good credit history | 1-2% | 12-14% |
| 650-699 | B+ | Good credit profile | 2-4% | 14-16% |
| 600-649 | B | Average creditworthiness | 4-7% | 16-18% |
| 550-599 | C+ | Below average, higher risk | 7-12% | 18-22% |
| 300-549 | C | Poor credit, very high risk | > 12% | Not listed |
Credit Assessment Service
public class CreditAssessmentService : ICreditAssessmentService
{
private readonly ICreditBureauClient _bureauClient;
private readonly IAlternativeDataClient _altDataClient;
private readonly ICreditScoringModel _scoringModel;
private readonly IAssessmentRepository _repository;
public async Task<CreditAssessmentResult> AssessAsync(Guid borrowerId)
{
var bureauData = await _bureauClient.PullCreditReportAsync(borrowerId);
var altData = await _altDataClient.GetAlternativeDataAsync(borrowerId);
var features = new CreditFeatures
{
BureauScore = bureauData.CibilScore,
DebtToIncomeRatio = CalculateDti(bureauData, altData),
EmploymentStability = altData.MonthsAtCurrentJob,
CreditHistoryLength = bureauData.CreditHistoryMonths,
UtilizationRatio = bureauData.CreditUtilization,
RecentInquiries = bureauData.RecentInquiries30Days,
OnTimePaymentRate = bureauData.OnTimePaymentPercentage,
BankBalanceStability = altData.BalanceStabilityIndex,
IncomeGrowth = altData.IncomeGrowthRate,
ExistingLoanCount = bureauData.ActiveLoans
};
var score = await _scoringModel.PredictAsync(features);
var grade = MapToGrade(score);
var rate = CalculateRate(grade, features);
var result = new CreditAssessmentResult
{
BorrowerId = borrowerId,
CreditScore = score,
RiskGrade = grade,
RecommendedRate = rate,
MaxLoanAmount = CalculateMaxLoan(grade, features),
DebtToIncomeRatio = features.DebtToIncomeRatio,
AssessmentDate = DateTime.UtcNow
};
await _repository.SaveAssessmentAsync(result);
return result;
}
private string MapToGrade(int score)
{
return score switch
{
>= 750 => "A+",
>= 700 => "A",
>= 650 => "B+",
>= 600 => "B",
>= 550 => "C+",
_ => "C"
};
}
private decimal CalculateRate(string grade, CreditFeatures features)
{
decimal baseRate = grade switch
{
"A+" => 10.0m, "A" => 12.0m, "B+" => 14.0m,
"B" => 16.0m, "C+" => 18.0m, _ => 22.0m
};
if (features.DebtToIncomeRatio > 0.5m) baseRate += 2.0m;
if (features.ExistingLoanCount > 5) baseRate += 1.0m;
return Math.Min(baseRate, 24.0m);
}
}
10. Loan Listing and Auction
Once a borrower passes credit assessment, the loan request is listed on the marketplace where investors can review and invest. The listing process must present all relevant information to investors while protecting borrower privacy. The platform supports both fixed-rate listings, where the platform sets the interest rate, and auction-based listings, where investors bid and the lowest competitive rate wins. The auction mechanism is particularly popular because it drives down borrowing costs through market competition while giving investors the opportunity to earn premium returns on higher-risk loans.
Loan Listing Data
| Field | Visible to Investors | Purpose |
|---|---|---|
| Loan Amount | Yes | How much the borrower needs |
| Purpose | Yes | Personal, business, education, medical |
| Risk Grade | Yes | Platform-assigned credit grade |
| Tenure | Yes | Number of months for repayment |
| Max Rate | Yes | Maximum rate borrower is willing to pay |
| Monthly Income | No | Used in scoring, not shown to protect privacy |
| Employment Type | Yes | Salaried or self-employed |
| City | Yes | Geographic diversification for investors |
| Funded Percentage | Yes | How much has been funded so far |
Auction Mechanism
In the auction model, borrowers set a maximum interest rate they are willing to pay, and investors bid with lower rates. The auction runs for a fixed period, typically 24 to 72 hours. At the end of the auction, the platform selects the lowest bids that together meet the loan amount. All winning bidders pay the same clearing rate, which is the highest accepted bid rate. This Dutch auction mechanism ensures fairness for both borrowers and investors.
public class AuctionService
{
private readonly IAuctionRepository _auctionRepo;
private readonly ILoanRepository _loanRepo;
private readonly IMessageBus _messageBus;
public async Task<AuctionResult> CloseAuctionAsync(Guid loanRequestId)
{
var auction = await _auctionRepo.GetActiveAuctionAsync(loanRequestId);
if (auction == null || auction.EndDate > DateTime.UtcNow)
throw new InvalidOperationException("Auction not ready to close");
var bids = await _auctionRepo.GetBidsAsync(loanRequestId);
var sortedBids = bids
.OrderBy(b => b.OfferedRate)
.ThenBy(b => b.SubmittedAt)
.ToList();
decimal totalFunded = 0m;
decimal clearingRate = 0m;
var winningBids = new List<AuctionBid>();
foreach (var bid in sortedBids)
{
if (totalFunded >= auction.RequestedAmount) break;
if (bid.OfferedRate > auction.BorrowerMaxRate) continue;
decimal remainingAmount = auction.RequestedAmount - totalFunded;
decimal allocatedAmount = Math.Min(bid.Amount, remainingAmount);
winningBids.Add(new AuctionBid
{
BidId = bid.Id,
InvestorId = bid.InvestorId,
AllocatedAmount = allocatedAmount,
FinalRate = bid.OfferedRate
});
totalFunded += allocatedAmount;
clearingRate = bid.OfferedRate;
}
foreach (var wb in winningBids)
wb.FinalRate = clearingRate;
var result = new AuctionResult
{
LoanRequestId = loanRequestId,
IsFullyFunded = totalFunded >= auction.RequestedAmount,
TotalFunded = totalFunded,
ClearingRate = clearingRate,
WinningBids = winningBids
};
await _auctionRepo.SaveAuctionResultAsync(result);
await _messageBus.PublishAsync(new AuctionClosedEvent { Result = result });
return result;
}
}
11. Investor Matching and Allocation
Investor matching determines how investor funds are allocated across loans to build diversified portfolios. The matching algorithm must consider the investor's risk appetite, desired returns, investment amount, portfolio diversification, and the available loan inventory. The goal is to maximize fund deployment while respecting investor preferences and maintaining portfolio health. The engine runs in two modes: real-time matching for manual investments and batch matching for auto-invest rules that execute daily during off-peak hours.
Allocation Algorithm
public class InvestorMatchingEngine
{
private readonly ILoanRepository _loanRepo;
private readonly IInvestorRepository _investorRepo;
private readonly IAllocationRepository _allocationRepo;
public async Task<List<AllocationResult>> MatchAndAllocateAsync()
{
var activeRules = await _investorRepo.GetActiveAutoInvestRulesAsync();
var availableLoans = await _loanRepo.GetListedLoansAsync();
var allocations = new List<AllocationResult>();
var sortedInvestors = activeRules
.OrderByDescending(r => r.Investor.VipStatus)
.ThenByDescending(r => r.Investor.AvailableBalance)
.ToList();
foreach (var rule in sortedInvestors)
{
if (rule.Investor.AvailableBalance < rule.MinInvestmentAmount) continue;
var matchingLoans = availableLoans
.Where(l => rule.AllowedRiskGrades.Contains(l.RiskGrade))
.Where(l => l.TenureMonths >= rule.MinTenureMonths)
.Where(l => l.TenureMonths <= rule.MaxTenureMonths)
.Where(l => l.InterestRate >= rule.MinExpectedRate)
.ToList();
decimal maxPerLoan = rule.Investor.AvailableBalance * rule.MaxConcentrationPercent;
matchingLoans = matchingLoans
.OrderBy(l => GetGradePriority(l.RiskGrade))
.ThenByDescending(l => l.InterestRate)
.ToList();
foreach (var loan in matchingLoans)
{
if (rule.Investor.AvailableBalance < rule.MinInvestmentAmount) break;
decimal investAmount = Math.Min(maxPerLoan, rule.Investor.AvailableBalance);
investAmount = Math.Min(investAmount, loan.RemainingFunding);
if (investAmount < rule.MinInvestmentAmount) continue;
allocations.Add(new AllocationResult
{
InvestorId = rule.InvestorId,
LoanId = loan.Id,
Amount = investAmount,
Rate = loan.InterestRate,
RiskGrade = loan.RiskGrade
});
rule.Investor.AvailableBalance -= investAmount;
}
}
return allocations;
}
private int GetGradePriority(string grade)
{
return grade switch
{
"A+" => 0, "A" => 1, "B+" => 2, "B" => 3, "C+" => 4, _ => 5
};
}
}
Diversification Rules
| Rule | Limit | Rationale |
|---|---|---|
| Max per loan | 25% of portfolio | Single loan default impact limit |
| Max per borrower | 25% of portfolio | Prevent concentration in one borrower |
| Max per risk grade | 40% of portfolio | Diversify across risk levels |
| Min loans in portfolio | 20 loans | Minimum diversification threshold |
| Max per city | 20% of portfolio | Geographic risk mitigation |
| Max per purpose | 30% of portfolio | Sector risk mitigation |
12. Escrow and Fund Management
Escrow management is the most critical component of a P2P lending platform from a trust and regulatory perspective. The RBI mandates that all investor funds must be held in a separate escrow account maintained with a scheduled commercial bank, and the platform cannot commingle these funds with its own operating capital. The escrow system must maintain accurate records of every investor's balance, process deposits and withdrawals with proper verification, and generate reconciliation reports that match the bank statements exactly. Any discrepancy between the platform's records and the bank balance must be flagged as a critical incident and investigated within 24 hours.
Escrow Account Structure
Escrow Service Implementation
public class EscrowService : IEscrowService
{
private readonly IEscrowRepository _escrowRepo;
private readonly IBankingGateway _bankingGateway;
private readonly IAuditLogger _auditLogger;
public async Task<EscrowTransaction> ProcessDepositAsync(
Guid investorId, decimal amount,
string bankReference, IdempotencyKey idempotencyKey)
{
var existing = await _escrowRepo.GetByIdempotencyKeyAsync(idempotencyKey);
if (existing != null) return existing;
var bankVerification = await _bankingGateway
.VerifyCreditAsync(bankReference, amount, investorId);
if (!bankVerification.IsVerified)
throw new FraudException("Bank credit verification failed");
var transaction = new EscrowTransaction
{
Id = Guid.NewGuid(),
InvestorId = investorId,
Amount = amount,
Type = EscrowTransactionType.Deposit,
Status = EscrowTransactionStatus.Completed,
BankReference = bankReference,
IdempotencyKey = idempotencyKey,
CreatedAt = DateTime.UtcNow
};
await _escrowRepo.ProcessAsync(transaction);
await _escrowRepo.CreditBalanceAsync(investorId, amount);
await _auditLogger.LogAsync(new AuditEntry
{
Action = "ESCROW_DEPOSIT",
EntityId = transaction.Id.ToString(),
UserId = investorId,
Metadata = new { Amount = amount, BankReference = bankReference }
});
return transaction;
}
public async Task<EscrowTransaction> ProcessDisbursementAsync(
Guid loanId, decimal amount, string borrowerBankAccount)
{
var totalCollected = await _escrowRepo
.GetTotalCollectedForLoanAsync(loanId);
if (totalCollected < amount)
throw new InsufficientFundsException();
var investments = await _escrowRepo
.GetInvestmentsForLoanAsync(loanId);
foreach (var investment in investments)
{
await _escrowRepo.DebitBalanceAsync(
investment.InvestorId, investment.Amount);
}
var bankResult = await _bankingGateway
.TransferFundsAsync(amount, borrowerBankAccount, $"Loan {loanId}");
if (!bankResult.IsSuccess)
throw new DisbursementException("Bank transfer failed");
var transaction = new EscrowTransaction
{
Id = Guid.NewGuid(),
LoanId = loanId,
Amount = amount,
Type = EscrowTransactionType.Disbursement,
Status = EscrowTransactionStatus.Completed,
BankReference = bankResult.Reference,
CreatedAt = DateTime.UtcNow
};
await _escrowRepo.ProcessAsync(transaction);
return transaction;
}
public async Task<ReconciliationResult> ReconcileAsync(DateTime date)
{
var dbBalance = await _escrowRepo.GetTotalBalanceAsync();
var bankBalance = await _bankingGateway.GetEscrowBalanceAsync(date);
var pendingTxns = await _escrowRepo.GetPendingTransactionsAsync(date);
var result = new ReconciliationResult
{
Date = date,
DatabaseBalance = dbBalance,
BankBalance = bankBalance,
Difference = dbBalance - bankBalance,
PendingCount = pendingTxns.Count,
IsReconciled = Math.Abs(dbBalance - bankBalance) < 0.01m
};
if (!result.IsReconciled)
{
await _auditLogger.AlertAsync(
$"Reconciliation mismatch: DB={dbBalance}, Bank={bankBalance}",
AlertSeverity.Critical);
}
return result;
}
}
13. Repayment Collection
Repayment collection is the process of receiving monthly EMI payments from borrowers and distributing them to investors. The platform supports multiple collection methods including NACH (National Automated Clearing House) auto-debit, UPI mandate, net banking, and manual payment. NACH is the primary method for recurring collections as it provides a structured, bank-backed mandate system with automatic retries on failure. The collection process must handle partial payments, handle failed debits with scheduled retries, and update the loan schedule accurately in all scenarios.
Collection Pipeline
Repayment Service
public class RepaymentCollectionService
{
private readonly ILoanRepository _loanRepo;
private readonly IEmiScheduleRepository _emiRepo;
private readonly INachGateway _nachGateway;
private readonly IInvestorPayoutService _payoutService;
private readonly IOverdueService _overdueService;
public async Task<BatchCollectionResult> ProcessDailyCollectionAsync(
DateTime collectionDate)
{
var dueEmis = await _emiRepo.GetDueEmisAsync(collectionDate);
var result = new BatchCollectionResult { Date = collectionDate };
var nachFile = await _nachGateway.GenerateNachFileAsync(dueEmis);
var submissionResult = await _nachGateway.SubmitFileAsync(nachFile);
foreach (var emi in dueEmis)
{
var debitResult = await _nachGateway
.CheckDebitStatusAsync(emi.LoanId, emi.EmiNumber);
if (debitResult.Status == NachDebitStatus.Success)
{
await ProcessSuccessfulCollectionAsync(emi, debitResult);
result.SuccessCount++;
result.TotalCollected += emi.TotalAmount;
}
else if (debitResult.Status == NachDebitStatus.InsufficientFunds)
{
await ScheduleRetryAsync(emi);
result.RetryCount++;
}
else
{
await HandleCollectionFailureAsync(emi, debitResult);
result.FailureCount++;
}
}
return result;
}
private async Task ProcessSuccessfulCollectionAsync(
EmiRecord emi, NachDebitResult debitResult)
{
emi.Status = EmiStatus.Paid;
emi.PaidDate = DateTime.UtcNow;
emi.PaidAmount = emi.TotalAmount;
emi.BankReference = debitResult.Reference;
await _emiRepo.UpdateAsync(emi);
var loan = await _loanRepo.GetByIdAsync(emi.LoanId);
loan.OutstandingPrincipal -= emi.PrincipalPart;
loan.PaidEmiCount++;
loan.DaysPastDue = 0;
if (loan.PaidEmiCount >= loan.TenureMonths)
loan.Status = LoanStatus.Closed;
await _loanRepo.UpdateAsync(loan);
await _payoutService.DistributeEmiAsync(emi.LoanId, emi);
}
private async Task HandleCollectionFailureAsync(
EmiRecord emi, NachDebitResult debitResult)
{
emi.Status = EmiStatus.Overdue;
emi.OverdueFrom = emi.DueDate.AddDays(1);
await _emiRepo.UpdateAsync(emi);
var loan = await _loanRepo.GetByIdAsync(emi.LoanId);
loan.DaysPastDue = (DateTime.UtcNow - emi.DueDate).Days;
await _loanRepo.UpdateAsync(loan);
await _overdueService.InitiateDunningAsync(emi.LoanId, emi.EmiNumber);
}
}
Investor Payout Distribution
public class InvestorPayoutService
{
public async Task DistributeEmiAsync(Guid loanId, EmiRecord emi)
{
var investments = await _investmentRepo.GetActiveInvestmentsAsync(loanId);
decimal totalInvested = investments.Sum(i => i.Amount);
foreach (var investment in investments)
{
decimal sharePercent = investment.Amount / totalInvested;
decimal principalPayout = Math.Round(emi.PrincipalPart * sharePercent, 2);
decimal interestPayout = Math.Round(emi.InterestPart * sharePercent, 2);
decimal platformFee = Math.Round(interestPayout * 0.01m, 2);
decimal netPayout = principalPayout + interestPayout - platformFee;
var payout = new InvestmentPayout
{
Id = Guid.NewGuid(),
InvestmentId = investment.Id,
LoanId = loanId,
EmiNumber = emi.EmiNumber,
PrincipalComponent = principalPayout,
InterestComponent = interestPayout,
PlatformFee = platformFee,
NetAmount = netPayout,
Status = PayoutStatus.Processing,
CreatedAt = DateTime.UtcNow
};
await _payoutRepo.SaveAsync(payout);
await _escrowService.CreditBalanceAsync(
investment.InvestorId, netPayout);
investment.ReturnsReceived += netPayout;
await _investmentRepo.UpdateAsync(investment);
}
}
}
14. Default and Recovery
Default management is a critical process that directly impacts investor returns and platform reputation. A loan is classified as overdue when the EMI is not paid by the due date. The RBI defines stages of delinquency that trigger different recovery actions. The platform must implement an automated dunning process that escalates through increasingly aggressive recovery measures, from gentle reminders to legal notices to engagement with recovery agents. The dunning system must be careful not to harass borrowers while still maintaining pressure to recover funds, as excessive collection tactics can result in regulatory penalties and reputational damage.
Delinquency Stages
| DPD Range | Classification | Action | Impact |
|---|---|---|---|
| 1-30 days | Overdue | SMS and email reminders, auto-retry debit | Minor - recoverable |
| 31-60 days | Substandard | Phone calls, field agent visit | Noticeable - early intervention needed |
| 61-90 days | Doubtful | Formal legal notice, recovery agency | Significant - recovery uncertain |
| 91-180 days | Loss Category | Debt collection agency, legal proceedings | Severe - partial recovery expected |
| 180+ days | Written Off | Final recovery attempt, tax write-off | Critical - investor loss |
Recovery Service
public class DefaultRecoveryService
{
private readonly ILoanRepository _loanRepo;
private readonly IRecoveryAgentClient _recoveryClient;
private readonly IDunningService _dunningService;
private readonly IInvestorNotificationService _notifService;
public async Task<DailyDefaultResult> ProcessDailyDefaultsAsync()
{
var overdueLoans = await _loanRepo.GetOverdueLoansAsync();
var result = new DailyDefaultResult();
foreach (var loan in overdueLoans)
{
int dpd = (DateTime.UtcNow - loan.LastPaymentDate).Days;
loan.DaysPastDue = dpd;
switch (dpd)
{
case int n when n >= 1 && n <= 30:
await _dunningService.SendRemindersAsync(loan);
result.StageOneCount++;
break;
case int n when n >= 31 && n <= 60:
await _dunningService.InitiatePhoneCallAsync(loan);
await _dunningService.ScheduleFieldVisitAsync(loan);
result.StageTwoCount++;
break;
case int n when n >= 61 && n <= 90:
await _dunningService.SendLegalNoticeAsync(loan);
await _recoveryClient.AssignAgentAsync(
loan.Id, RecoveryPriority.High);
result.StageThreeCount++;
break;
case int n when n >= 91 && n < 180:
await _recoveryClient.InitiateLegalProceedingAsync(loan.Id);
result.StageFourCount++;
break;
case int n when n >= 180:
await ProcessChargeOffAsync(loan);
result.ChargeOffCount++;
break;
}
await _loanRepo.UpdateAsync(loan);
}
await _notifService.NotifyInvestorsOfDefaultsAsync(overdueLoans);
return result;
}
private async Task ProcessChargeOffAsync(Loan loan)
{
loan.Status = LoanStatus.WrittenOff;
loan.WrittenOffDate = DateTime.UtcNow;
loan.WriteOffAmount = loan.OutstandingPrincipal;
var investments = await _investmentRepo
.GetActiveInvestmentsAsync(loan.Id);
foreach (var investment in investments)
{
decimal loss = investment.Amount *
(loan.OutstandingPrincipal / loan.PrincipalAmount);
investment.Status = InvestmentStatus.WrittenOff;
investment.LossAmount = loss;
await _investmentRepo.UpdateAsync(investment);
}
await _recoveryClient.InitiateFinalRecoveryAsync(loan.Id);
}
}
15. Auto-Invest Rules Engine
The auto-invest feature allows investors to set rules that automatically invest their idle funds in matching loans. This increases fund utilization and improves investor returns. The rules engine evaluates each new loan listing against all active investor rules and triggers investments for matches. The engine must handle concurrent execution, prevent overspending, and respect diversification limits. The auto-invest engine is one of the most latency-sensitive components because it must evaluate hundreds of investor rules within seconds of a new loan being listed.
Auto-Invest Rule Configuration
public class AutoInvestRule
{
public Guid Id { get; set; }
public Guid InvestorId { get; set; }
public bool IsActive { get; set; }
public decimal MinInvestmentAmount { get; set; }
public decimal MaxInvestmentPerLoan { get; set; }
public decimal MaxTotalInvestment { get; set; }
public string[] AllowedRiskGrades { get; set; }
public decimal MinExpectedRate { get; set; }
public int MinTenureMonths { get; set; }
public int MaxTenureMonths { get; set; }
public string[] AllowedPurposes { get; set; }
public decimal MaxConcentrationPercent { get; set; }
public int MinPortfolioSize { get; set; }
public string[] ExcludedBorrowerIds { get; set; }
public AutoInvestFrequency Frequency { get; set; }
public TimeSpan? PreferredExecutionTime { get; set; }
}
Rule Evaluation Engine
public class AutoInvestEngine
{
private readonly IRuleRepository _ruleRepo;
private readonly ILoanRepository _loanRepo;
private readonly IInvestmentService _investmentService;
private readonly IEscrowService _escrowService;
private readonly IDistributedLock _lockManager;
public async Task ProcessLoanListingAsync(Guid loanRequestId)
{
var loan = await _loanRepo.GetLoanRequestAsync(loanRequestId);
var matchingRules = await _ruleRepo.GetMatchingRulesAsync(loan);
foreach (var rule in matchingRules.OrderBy(r => r.Investor.Priority))
{
var lockKey = $"auto-invest:{rule.InvestorId}";
using (await _lockManager.AcquireAsync(lockKey, TimeSpan.FromSeconds(30)))
{
var balance = await _escrowService
.GetAvailableBalanceAsync(rule.InvestorId);
if (balance < rule.MinInvestmentAmount) continue;
var currentExposure = await _investmentService
.GetCurrentExposureAsync(rule.InvestorId, loan.BorrowerId);
var maxExposure = rule.Investor.TotalInvested *
rule.MaxConcentrationPercent / 100;
if (currentExposure >= maxExposure) continue;
decimal investAmount = Math.Min(
rule.MaxInvestmentPerLoan, balance);
investAmount = Math.Min(
investAmount, maxExposure - currentExposure);
investAmount = Math.Min(
investAmount, loan.RemainingFunding);
if (investAmount < rule.MinInvestmentAmount) continue;
await _investmentService.PlaceAutoInvestmentAsync(
rule.InvestorId, loanRequestId, investAmount);
}
}
}
}
Auto-Invest Frequency Options
| Frequency | Description | Best For |
|---|---|---|
| RealTime | Invest immediately when loan matches rules | Investors with idle funds wanting max deployment |
| Daily | Run matching once daily during off-peak hours | Conservative investors who want to review allocations |
| Weekly | Run matching once per week | Passive investors who check portfolio infrequently |
16. Secondary Market and Loan Trading
The secondary market allows investors to sell their loan parts to other investors before the loan matures. This provides liquidity to investors who need early exit and creates trading opportunities for investors seeking specific risk-return profiles. The secondary market must handle fair pricing based on remaining cash flows and current market conditions, ownership transfer that updates all downstream payment distributions, and regulatory compliance since the RBI has specific guidelines on secondary market transactions for P2P loans. The secondary market is essential for investor retention because without an exit option, many investors would not commit funds to longer-tenure loans.
Secondary Market Design
public class SecondaryMarketService
{
private readonly ILoanPartRepository _loanPartRepo;
private readonly IPriceDiscoveryEngine _priceEngine;
private readonly ITransferService _transferService;
private readonly IInvestorPayoutService _payoutService;
public async Task<ListingResult> CreateListingAsync(
Guid sellerId, Guid investmentId, decimal askingPrice)
{
var investment = await _loanPartRepo
.GetInvestmentAsync(investmentId);
if (investment.InvestorId != sellerId)
throw new UnauthorizedException("Not the owner");
if (investment.Status != InvestmentStatus.Active)
throw new InvalidOperationException("Loan part not active");
var fairValue = await _priceEngine
.CalculateFairValueAsync(investment);
if (askingPrice < fairValue * 0.9m)
throw new ValidationException("Asking price too low");
var listing = new SecondaryMarketListing
{
Id = Guid.NewGuid(),
SellerId = sellerId,
InvestmentId = investmentId,
LoanId = investment.LoanId,
Units = investment.Amount,
AskingPrice = askingPrice,
FairValue = fairValue,
OutstandingPrincipal = investment.OutstandingPrincipal,
MonthlyEmi = investment.MonthlyEmi,
RemainingTenure = investment.RemainingTenure,
RiskGrade = investment.RiskGrade,
Status = ListingStatus.Active,
ListedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddHours(48)
};
await _loanPartRepo.SaveListingAsync(listing);
return new ListingResult
{
ListingId = listing.Id,
FairValue = fairValue
};
}
public async Task<TradeResult> ExecuteTradeAsync(
Guid listingId, Guid buyerId)
{
var listing = await _loanPartRepo.GetListingAsync(listingId);
if (listing.Status != ListingStatus.Active)
throw new InvalidOperationException("Listing not active");
if (listing.ExpiresAt < DateTime.UtcNow)
throw new InvalidOperationException("Listing expired");
var buyerBalance = await _escrowService
.GetAvailableBalanceAsync(buyerId);
if (buyerBalance < listing.AskingPrice)
throw new InsufficientFundsException();
await _transferService.ExecuteTradeAsync(
buyerId, listing.SellerId, listing.AskingPrice);
var buyerInvestment = new Investment
{
Id = Guid.NewGuid(),
InvestorId = buyerId,
LoanId = listing.LoanId,
Amount = listing.Units,
Status = InvestmentStatus.Active,
AcquiredVia = "SecondaryMarket",
OriginalInvestmentId = listing.InvestmentId,
CreatedAt = DateTime.UtcNow
};
await _loanPartRepo.SaveInvestmentAsync(buyerInvestment);
var sellerInvestment = await _loanPartRepo
.GetInvestmentAsync(listing.InvestmentId);
sellerInvestment.Status = InvestmentStatus.Sold;
sellerInvestment.SoldAt = DateTime.UtcNow;
await _loanPartRepo.UpdateInvestmentAsync(sellerInvestment);
listing.Status = ListingStatus.Sold;
listing.BuyerId = buyerId;
listing.SoldAt = DateTime.UtcNow;
await _loanPartRepo.UpdateListingAsync(listing);
return new TradeResult
{
TradeId = Guid.NewGuid(),
ListingId = listingId,
BuyerId = buyerId,
SellerId = listing.SellerId,
Amount = listing.AskingPrice,
ExecutedAt = DateTime.UtcNow
};
}
}
17. Investor Dashboard
The investor dashboard provides a comprehensive view of the investor's portfolio, including current holdings, returns, default status, cash flow projections, and available balance. The dashboard must load quickly with real-time data and support drill-down into individual loans. Performance is critical because investors check their dashboards frequently, especially during market volatility or when repayment cycles are running. The dashboard aggregates data from multiple services and presents it in a unified, actionable format.
Dashboard Data Model
public class InvestorDashboard
{
public decimal TotalInvested { get; set; }
public decimal CurrentValue { get; set; }
public decimal TotalReturns { get; set; }
public decimal TotalDefaults { get; set; }
public decimal NetReturn { get; set; }
public decimal AnnualizedYield { get; set; }
public decimal Xirr { get; set; }
public PortfolioBreakdown ByRiskGrade { get; set; }
public PortfolioBreakdown ByPurpose { get; set; }
public PortfolioBreakdown ByTenure { get; set; }
public int ActiveLoans { get; set; }
public int OverdueLoans { get; set; }
public int DefaultedLoans { get; set; }
public decimal DefaultRate { get; set; }
public decimal WeightedAverageRate { get; set; }
public decimal WeightedAverageTenure { get; set; }
public List<MonthlyCashFlow> FutureCashFlows { get; set; }
public List<RecentTransaction> RecentActivity { get; set; }
public decimal AvailableBalance { get; set; }
public decimal PendingPayouts { get; set; }
public decimal InvestedInEscrow { get; set; }
}
public class MonthlyCashFlow
{
public int Month { get; set; }
public decimal PrincipalReturn { get; set; }
public decimal InterestIncome { get; set; }
public decimal TotalInflow { get; set; }
public int ActiveLoanCount { get; set; }
}
public class DashboardService
{
private readonly IDashboardRepository _repo;
private readonly IDistributedCache _cache;
public async Task<InvestorDashboard> GetDashboardAsync(Guid investorId)
{
var cacheKey = $"dashboard:{investorId}";
var cached = await _cache.GetAsync<InvestorDashboard>(cacheKey);
if (cached != null) return cached;
var dashboard = await _repo.BuildDashboardAsync(investorId);
await _cache.SetAsync(cacheKey, dashboard,
TimeSpan.FromMinutes(5));
return dashboard;
}
}
Dashboard Performance Metrics
| Metric | Target | Strategy |
|---|---|---|
| Dashboard load time | < 2 seconds | Redis cache, read replicas, CDN for static assets |
| Cash flow projection | < 500ms | Pre-computed daily, cached in Redis |
| XIRR calculation | < 3 seconds | Background job, result cached for 1 hour |
| Portfolio breakdown | < 1 second | Materialized views updated every 15 minutes |
| Recent activity | < 200ms | Last 50 transactions cached per user |
18. Regulatory Compliance — RBI NBFC-P2P
The Reserve Bank of India (RBI) introduced comprehensive guidelines for NBFC-P2P lending platforms in 2017, subsequently amended in 2019 and 2022. These regulations establish the rules for platform operation, investor limits, borrowing limits, fund flow requirements, and reporting obligations. Non-compliance can result in penalties, license revocation, or criminal prosecution. Every engineering decision in the platform must be made with regulatory compliance in mind. The regulatory landscape for P2P lending in India is complex and evolving, requiring close coordination between the legal, compliance, and engineering teams.
Key RBI Regulations
| Regulation | Requirement | Implementation |
|---|---|---|
| Investor Limit | Max 10 lakh per borrower across all platforms | Real-time aggregation check via bureau data |
| Borrower Limit | Max 50 lakh total borrowing across all platforms | Bureau pull plus self-declaration |
| Interest Rate Cap | Max 24% per annum all-inclusive | System-enforced hard cap in pricing engine |
| Loan Tenure | Max 36 months | Hard cap in loan application validation |
| Escrow Requirement | Separate escrow with scheduled commercial bank | Bank partnership, daily reconciliation |
| No Principal Guarantee | Platform cannot guarantee returns to investors | Clear risk disclosure, no guaranteed return products |
| Information Utility | Register with RBI Information Utility | API integration for loan registration |
| Reporting | Quarterly returns to RBI | Automated report generation and submission |
| Data Localization | All data must reside in India | India-only cloud regions, no cross-border data transfer |
| Capital Adequacy | Minimum net owned fund of 2 crore | Monthly capital adequacy monitoring dashboard |
Compliance Service Implementation
public class RegulatoryComplianceService
{
private readonly IBorrowerRepository _borrowerRepo;
private readonly IInvestorRepository _investorRepo;
private readonly IBureauClient _bureauClient;
private readonly IRbiReportingClient _rbiClient;
public async Task<ComplianceCheckResult> ValidateBorrowingLimitAsync(
Guid borrowerId, decimal requestedAmount)
{
var borrower = await _borrowerRepo.GetByIdAsync(borrowerId);
// Check total outstanding across all P2P platforms via bureau
var bureauReport = await _bureauClient
.GetPeerToPeerExposureAsync(borrower.PanNumber);
decimal existingExposure = bureauReport.P2PLoanOutstanding;
decimal proposedTotal = existingExposure + requestedAmount;
var result = new ComplianceCheckResult();
// RBI limit: max 50 lakh total P2P borrowing
if (proposedTotal > 5_00_000m)
{
result.IsCompliant = false;
result.Violation = "Exceeds RBI borrowing limit of 10 lakh";
result.MaxAllowed = 5_00_000m - existingExposure;
return result;
}
// Check interest rate cap
var assessment = await _borrowerRepo
.GetLatestAssessmentAsync(borrowerId);
if (assessment.RecommendedRate > 24.0m)
{
result.IsCompliant = false;
result.Violation = "Rate exceeds RBI cap of 24%";
return result;
}
result.IsCompliant = true;
return result;
}
public async Task<ComplianceCheckResult> ValidateInvestorLimitAsync(
Guid investorId, Guid borrowerId, decimal investmentAmount)
{
// Check per-borrower limit across all platforms
var existingExposure = await _investorRepo
.GetExposureToBorrowerAsync(investorId, borrowerId);
decimal proposedTotal = existingExposure + investmentAmount;
// RBI limit: max 10 lakh per borrower
if (proposedTotal > 10_00_000m)
{
return new ComplianceCheckResult
{
IsCompliant = false,
Violation = "Exceeds RBI investor limit of 10 lakh per borrower",
MaxAllowed = 10_00_000m - existingExposure
};
}
return new ComplianceCheckResult { IsCompliant = true };
}
public async Task GenerateQuarterlyReportAsync(DateTime quarterEnd)
{
var report = new RbiQuarterlyReport
{
ReportingPeriod = quarterEnd,
TotalLoansOriginated = await GetOriginatedCountAsync(quarterEnd),
TotalAmountDisbursed = await GetDisbursedAmountAsync(quarterEnd),
AverageLoanSize = await GetAverageLoanSizeAsync(quarterEnd),
AverageInterestRate = await GetAverageRateAsync(quarterEnd),
NPAPercentage = await GetNpaPercentageAsync(quarterEnd),
TotalInvestors = await GetActiveInvestorCountAsync(),
TotalBorrowers = await GetActiveBorrowerCountAsync(),
EscrowBalance = await GetEscrowBalanceAsync(quarterEnd),
CapitalAdequacyRatio = await GetCarAsync(),
ComplaintCount = await GetComplaintCountAsync(quarterEnd)
};
await _rbiClient.SubmitReportAsync(report);
}
}
Reporting Schedule
| Report | Frequency | Deadline | Recipient |
|---|---|---|---|
| Quarterly Returns | Quarterly | 15 days after quarter end | RBI Regional Office |
| Annual Returns | Annually | March 31 | RBI |
| Audit Report | Annually | June 30 | RBI |
| Escrow Reconciliation | Quarterly | 30 days after quarter end | Auditor + RBI |
| Capital Adequacy | Monthly | 10th of following month | Internal |
| Fraud Report | As needed | Within 24 hours | RBI + Law Enforcement |
19. Fraud Prevention and Detection
Fraud prevention is critical in P2P lending because the platform facilitates direct financial transactions between individuals. Common fraud vectors include identity theft (using stolen KYC documents to create fake borrower accounts), income fraud (inflating income to qualify for larger loans), collusion fraud (a single person creating multiple borrower and investor accounts to manipulate the system), and money laundering (using the platform to move funds between controlled accounts). The fraud detection system must operate in real-time for transaction monitoring and in batch mode for pattern detection across the entire user base.
Fraud Detection Rules
| Rule | Detection Method | Action |
|---|---|---|
| Duplicate PAN | Exact match on PAN across accounts | Block registration, alert compliance |
| Duplicate Aadhaar | Hash match on Aadhaar number | Block registration, alert compliance |
| Velocity check | Multiple loan applications in short period | Flag for manual review |
| Income anomaly | Reported income vs bank statement | Request additional proof, flag if discrepancy > 30% |
| Device fingerprint | Multiple accounts from same device | Alert, investigate for collusion |
| Geolocation | IP address mismatch with registered address | Additional verification step |
| Collusion pattern | Graph analysis of borrower-investor relationships | Freeze accounts, compliance investigation |
| Behavioral biometrics | Typing patterns, navigation behavior | Flag anomalies for review |
| Bank statement manipulation | PDF metadata analysis, digital signature check | Reject document, request direct bank API access |
Fraud Detection Service
public class FraudDetectionService
{
private readonly IFraudRuleEngine _ruleEngine;
private readonly IDeviceFingerprintService _deviceService;
private readonly IGraphAnalysisService _graphService;
private readonly IFraudRepository _fraudRepo;
public async Task<FraudAssessmentResult> AssessRegistrationAsync(
RegistrationRequest request)
{
var result = new FraudAssessmentResult();
// Rule 1: Duplicate identity check
var existingUser = await _fraudRepo
.FindByPanAsync(request.PanNumber);
if (existingUser != null)
{
result.RiskScore += 90;
result.Flags.Add("DUPLICATE_PAN");
}
// Rule 2: Device fingerprint check
var deviceUsers = await _deviceService
.FindUsersByDeviceAsync(request.DeviceFingerprint);
if (deviceUsers.Count > 3)
{
result.RiskScore += 70;
result.Flags.Add("MULTIPLE_ACCOUNTS_SAME_DEVICE");
}
// Rule 3: IP reputation check
var ipInfo = await _deviceService.GetIpInfoAsync(request.IpAddress);
if (ipInfo.IsVpn || ipInfo.IsProxy)
{
result.RiskScore += 40;
result.Flags.Add("VPN_OR_PROXY_DETECTED");
}
// Rule 4: Email domain reputation
if (IsDisposableEmail(request.Email))
{
result.RiskScore += 50;
result.Flags.Add("DISPOSABLE_EMAIL");
}
// Determine action
result.Action = result.RiskScore switch
{
>= 80 => FraudAction.Block,
>= 50 => FraudAction.ManualReview,
>= 30 => FraudAction.AdditionalVerification,
_ => FraudAction.Approve
};
await _fraudRepo.SaveAssessmentAsync(request.UserId, result);
return result;
}
public async Task DetectCollusionPatternsAsync()
{
var graph = await _graphService.BuildLendingGraphAsync();
// Find strongly connected components
var sccs = graph.FindStronglyConnectedComponents();
foreach (var component in sccs)
{
if (component.NodeCount > 3)
{
// Potential collusion ring
await _fraudRepo.RaiseCollusionAlertAsync(
component.UserIds,
$"Suspected collusion ring with {component.NodeCount} users");
// Freeze all accounts in the ring
foreach (var userId in component.UserIds)
{
await _fraudRepo.FreezeAccountAsync(userId,
"Suspected collusion - under investigation");
}
}
}
}
}
20. Documentation and E-Sign
Every P2P loan must be accompanied by a legally binding loan agreement that specifies the loan amount, interest rate, tenure, repayment schedule, default consequences, and the rights and obligations of both borrower and investors. The RBI requires that these agreements be executed with digital signatures using Digital Signature Certificates (DSC) issued by licensed certifying authorities. The document management system must generate templated agreements, present them for e-signature, store the signed documents securely, and provide them for download by all parties.
Document Generation Pipeline
Document Service Implementation
public class DocumentService
{
private readonly ITemplateEngine _templateEngine;
private readonly IPdfGenerator _pdfGenerator;
private readonly IEsignProvider _esignProvider;
private readonly IBlobStorage _blobStorage;
private readonly IDocumentRepository _docRepo;
public async Task<LoanAgreement> GenerateAndSignAsync(Guid loanId)
{
var loan = await _loanRepo.GetByIdAsync(loanId);
var borrower = await _borrowerRepo.GetByIdAsync(loan.BorrowerId);
var investors = await _investmentRepo.GetInvestorsAsync(loanId);
// Generate agreement from template
var templateData = new AgreementTemplateData
{
LoanId = loanId.ToString(),
BorrowerName = borrower.FullName,
BorrowerPan = borrower.PanNumber,
LoanAmount = loan.PrincipalAmount,
InterestRate = loan.AnnualInterestRate,
TenureMonths = loan.TenureMonths,
MonthlyEmi = loan.MonthlyEmi,
DisbursementDate = loan.DisbursedAt.ToString("dd MMMM yyyy"),
MaturityDate = loan.MaturityDate.ToString("dd MMMM yyyy"),
InvestorNames = investors.Select(i => i.FullName).ToList(),
PlatformName = "P2P Lending Platform",
AgreementDate = DateTime.UtcNow.ToString("dd MMMM yyyy")
};
var htmlContent = await _templateEngine.RenderAsync(
"LoanAgreement", templateData);
var pdfBytes = await _pdfGenerator.GenerateAsync(htmlContent);
// Store PDF in blob storage
var blobPath = $"agreements/{loanId}/loan_agreement_v1.pdf";
await _blobStorage.UploadAsync(blobPath, pdfBytes, "application/pdf");
// Send for e-signature to borrower
var esignResult = await _esignProvider.SendForSigningAsync(
new EsignRequest
{
DocumentPath = blobPath,
SignerEmail = borrower.Email,
SignerName = borrower.FullName,
ExpiryHours = 72
});
var agreement = new LoanAgreement
{
Id = Guid.NewGuid(),
LoanId = loanId,
Version = 1,
FilePath = blobPath,
EsignRequestId = esignResult.RequestId,
Status = AgreementStatus.PendingSignature,
CreatedAt = DateTime.UtcNow
};
await _docRepo.SaveAsync(agreement);
return agreement;
}
public async Task OnEsignCompletedAsync(string esignRequestId)
{
var agreement = await _docRepo
.GetByEsignRequestIdAsync(esignRequestId);
agreement.Status = AgreementStatus.Signed;
agreement.SignedAt = DateTime.UtcNow;
await _docRepo.UpdateAsync(agreement);
// Notify all parties
var loan = await _loanRepo.GetByIdAsync(agreement.LoanId);
await _notifService.NotifyAsync(loan.BorrowerId,
"Your loan agreement has been signed successfully.");
}
}
E-Signature Provider Comparison
| Provider | Compliance | Cost per Sign | Integration |
|---|---|---|---|
| Leegality | ITA 2000 compliant | 5 INR | REST API + SDK |
| SignDesk | ITA 2000 compliant | 8 INR | REST API |
| Emudhra | Certifying Authority licensed | 15 INR | DSC + API |
| NeSL | RBI approved | 10 INR | API |
21. Notifications and Communication
The notification system must deliver timely, relevant messages to borrowers and investors across multiple channels. Notifications are triggered by platform events such as KYC approval, loan listing, investment confirmation, EMI due reminders, payment confirmations, and default alerts. Each notification must be delivered through the preferred channel (SMS, email, push notification) with fallback mechanisms if the primary channel fails. The system must respect user preferences for notification frequency and comply with TRAI regulations for commercial SMS messages.
Notification Templates
| Event | Channel | Template | Priority |
|---|---|---|---|
| KYC Approved | Email + Push | Welcome message with next steps | High |
| Loan Listed | Push | New loan available notification | Medium |
| Investment Confirmed | SMS + Email | Investment receipt with details | High |
| EMI Due Reminder | SMS + Push | 3-day and 1-day before EMI | High |
| Payment Received | SMS + Email | Payment confirmation and receipt | High |
| Default Alert | Loan overdue notification | Critical | |
| Monthly Statement | Portfolio performance report | Low | |
| Regulatory Update | Policy changes or compliance notices | Medium |
Notification Service
public class NotificationService
{
private readonly ISmsGateway _smsGateway;
private readonly IEmailGateway _emailGateway;
private readonly IPushNotificationService _pushService;
private readonly INotificationRepository _notifRepo;
private readonly IUserPreferenceRepository _prefRepo;
public async Task SendAsync(NotificationEvent notification)
{
var user = await _prefRepo.GetUserAsync(notification.UserId);
var preferences = await _prefRepo
.GetPreferencesAsync(notification.UserId);
if (!preferences.IsChannelEnabled(notification.Channel))
return;
var template = await GetTemplateAsync(
notification.EventType, notification.Channel);
var content = template.Render(notification.Data);
NotificationRecord record = notification.Channel switch
{
Channel.Sms => await SendSmsAsync(user.Phone, content),
Channel.Email => await SendEmailAsync(
user.Email, template.Subject, content),
Channel.Push => await SendPushAsync(
user.DeviceTokens, template.Title, content),
_ => throw new ArgumentException("Unknown channel")
};
await _notifRepo.SaveAsync(record);
}
private async Task<NotificationRecord> SendSmsAsync(
string phone, string content)
{
var result = await _smsGateway.SendAsync(phone, content);
return new NotificationRecord
{
Id = Guid.NewGuid(),
Channel = Channel.Sms,
Recipient = phone,
Content = content,
Status = result.IsSuccess
? NotificationStatus.Delivered
: NotificationStatus.Failed,
SentAt = DateTime.UtcNow,
ProviderMessageId = result.MessageId
};
}
}
22. Analytics and Reporting
The analytics platform provides insights into platform performance, investor returns, borrower behavior, and operational metrics. The analytics system processes data from multiple sources, aggregates it into meaningful metrics, and presents it through dashboards and automated reports. Key stakeholders include the platform management team (for business metrics), the compliance team (for regulatory reporting), the risk team (for portfolio health), and investor relations (for investor communication). The analytics pipeline must handle both real-time dashboards and batch reporting with appropriate technology choices for each use case.
Key Platform Metrics
| Metric | Formula | Target | Alert Threshold |
|---|---|---|---|
| Default Rate | Defaults / Total Originations | < 3% | > 5% |
| Average Yield | Weighted average investor return | 12-16% | < 10% |
| Recovery Rate | Amount recovered / Amount defaulted | > 40% | < 25% |
| Fund Utilization | Funded loans / Listed loans | > 80% | < 60% |
| Avg Time to Fund | Average hours from listing to fully funded | < 48 hours | > 96 hours |
| Investor Retention | Active investors / Total registered | > 60% | < 40% |
| Borrower NPS | Net Promoter Score | > 50 | < 30 |
| Collection Efficiency | Amount collected / Amount due | > 97% | < 93% |
Analytics Pipeline Architecture
public class AnalyticsPipeline
{
private readonly IKafkaConsumer _kafkaConsumer;
private readonly IAnalyticsRepository _analyticsRepo;
private readonly IDashboardCache _dashboardCache;
public async Task ProcessEventAsync(PlatformEvent evt)
{
switch (evt)
{
case LoanDisbursedEvent e:
await ProcessLoanDisbursed(e);
break;
case EmiReceivedEvent e:
await ProcessEmiReceived(e);
break;
case LoanDefaultedEvent e:
await ProcessLoanDefaulted(e);
break;
case InvestmentPlacedEvent e:
await ProcessInvestmentPlaced(e);
break;
}
// Update real-time counters
await _analyticsRepo.IncrementCounterAsync(
evt.EventType, evt.Timestamp.Date);
// Invalidate dashboard caches
await _dashboardCache.InvalidateRelatedCachesAsync(evt);
}
private async Task ProcessLoanDisbursed(LoanDisbursedEvent e)
{
await _analyticsRepo.RecordMetricAsync("originations",
e.Amount, e.Timestamp);
await _analyticsRepo.UpdatePortfolioMetricAsync(
e.RiskGrade, e.Amount, "disbursed");
}
private async Task ProcessEmiReceived(EmiReceivedEvent e)
{
await _analyticsRepo.RecordMetricAsync("collections",
e.Amount, e.Timestamp);
await _analyticsRepo.UpdateCollectionEfficiencyAsync(
e.LoanId, e.Amount, e.DueAmount);
}
public async Task GenerateDailyReportAsync(DateTime date)
{
var report = new DailyReport
{
Date = date,
NewRegistrations = await _analyticsRepo
.GetCounterAsync("registrations", date),
LoansDisbursed = await _analyticsRepo
.GetCounterAsync("originations", date),
AmountDisbursed = await _analyticsRepo
.GetMetricSumAsync("originations", date),
CollectionsReceived = await _analyticsRepo
.GetMetricSumAsync("collections", date),
DefaultAlerts = await _analyticsRepo
.GetCounterAsync("defaults", date),
InvestorPayouts = await _analyticsRepo
.GetMetricSumAsync("payouts", date)
};
await _analyticsRepo.SaveDailyReportAsync(report);
await SendReportToStakeholdersAsync(report);
}
}
23. Cost Estimation
Understanding the cost structure of a P2P lending platform is essential for financial planning and investor communication. The cost of running the platform includes infrastructure costs (cloud computing, databases, storage), third-party integration costs (KYC verification, credit bureau pulls, payment gateway fees), operational costs (recovery agents, customer support, compliance), and technology costs (development, maintenance, security audits). These costs must be recovered through platform fees while keeping the service competitive enough to attract both borrowers and investors.
Infrastructure Cost Breakdown (Monthly)
| Component | Specification | Monthly Cost |
|---|---|---|
| Application Servers (3x) | 4 vCPU, 16GB RAM | 15,000 INR |
| PostgreSQL Primary | 8 vCPU, 64GB RAM, 1TB SSD | 25,000 INR |
| PostgreSQL Replica | 8 vCPU, 64GB RAM, 1TB SSD | 25,000 INR |
| Redis Cluster (3x) | 16GB RAM each | 12,000 INR |
| Elasticsearch (5x) | 16 vCPU, 64GB RAM, 500GB SSD | 40,000 INR |
| Kafka Cluster (3x) | 4 vCPU, 16GB RAM | 12,000 INR |
| Object Storage (S3) | 5TB | 5,000 INR |
| CDN | 500GB bandwidth | 3,000 INR |
| Monitoring (Datadog) | Full stack | 15,000 INR |
| SSL Certificates | Wildcard | 1,000 INR |
| Total Infrastructure | 153,000 INR |
Third-Party Integration Costs (Monthly)
| Service | Volume | Cost per Unit | Monthly Cost |
|---|---|---|---|
| KYC Verification | 25,000 checks | 15 INR | 3,75,000 INR |
| Credit Bureau Pull | 9,000 pulls | 50 INR | 4,50,000 INR |
| SMS Gateway | 5,00,000 messages | 0.20 INR | 1,00,000 INR |
| Email Service | 2,00,000 emails | 0.10 INR | 20,000 INR |
| NACH Processing | 2,40,000 debits | 2 INR | 4,80,000 INR |
| E-Signature | 4,500 signatures | 8 INR | 36,000 INR |
| Document Storage | 5TB | 2 INR/GB | 10,000 INR |
| Total Third-Party | 14,71,000 INR |
Revenue Model
With 54,000 loans originated annually at an average loan size of 2,00,000 INR, the total annual originations are approximately 108 crore INR. The borrower processing fee at 2% generates 2.16 crore INR annually. The investor commission at 1% of all outstanding portfolio generates approximately 3.6 crore INR annually (assuming an average outstanding portfolio of 36 crore INR). Additional revenue from late fees, prepayment penalties, and premium features adds approximately 50 lakh INR annually. The total annual revenue is approximately 6.26 crore INR against an annual operating cost of approximately 2 crore INR, resulting in a healthy operating margin of approximately 68%.
24. Testing Strategy
Testing a P2P lending platform requires extreme rigor because software defects can lead to financial losses, regulatory violations, and loss of investor trust. The testing strategy must cover unit tests for business logic, integration tests for external services, end-to-end tests for critical workflows, load tests for performance validation, and chaos tests for resilience verification. Every financial calculation must have comprehensive test coverage, and every edge case must be explicitly tested.
Testing Layers
| Layer | Scope | Target Coverage | Tool |
|---|---|---|---|
| Unit Tests | Individual services and functions | 90% | xUnit + Moq |
| Integration Tests | Service interactions with real DB | 80% | Testcontainers |
| Contract Tests | API contract verification | 100% | Pact |
| E2E Tests | Complete user workflows | Critical paths | Selenium + Playwright |
| Load Tests | Performance under load | P99 < 500ms | k6 |
| Chaos Tests | Fault injection | Key failure modes | Chaos Monkey |
| Security Tests | Vulnerability scanning | OWASP Top 10 | OWASP ZAP |
Financial Calculation Tests
public class EmiCalculationTests
{
[Fact]
public void CalculateEmi_StandardLoan_ReturnsCorrectAmount()
{
// Arrange
decimal principal = 100000m;
decimal annualRate = 12m;
int tenureMonths = 12;
// Act
decimal emi = EmiCalculator.Calculate(
principal, annualRate, tenureMonths);
// Assert - using standard EMI formula
// EMI = P * r * (1+r)^n / ((1+r)^n - 1)
decimal expected = 8792m;
Assert.Equal(expected, Math.Round(emi, 0));
}
[Fact]
public void CalculateEmi_ZeroRate_ReturnsPrincipalDividedByTenure()
{
decimal emi = EmiCalculator.Calculate(120000m, 0m, 12);
Assert.Equal(10000m, emi);
}
[Fact]
public void EmiSchedule_TotalPrincipalEqualsLoanAmount()
{
var schedule = EmiCalculator.GenerateSchedule(
100000m, 12m, 12);
decimal totalPrincipal = schedule.Sum(e => e.PrincipalPart);
Assert.Equal(100000m, Math.Round(totalPrincipal, 2));
}
[Fact]
public void EmiSchedule_AmountsNeverNegative()
{
var schedule = EmiCalculator.GenerateSchedule(
50000m, 18m, 24);
Assert.All(schedule, e =>
{
Assert.True(e.PrincipalPart > 0);
Assert.True(e.InterestPart > 0);
Assert.True(e.TotalAmount == e.PrincipalPart + e.InterestPart);
});
}
}
public class EscrowReconciliationTests
{
[Fact]
public void Reconcile_MatchingBalances_ReturnsReconciled()
{
var dbBalance = 5000000m;
var bankBalance = 5000000m;
var result = Reconciler.Check(dbBalance, bankBalance);
Assert.True(result.IsReconciled);
Assert.Equal(0m, result.Difference);
}
[Fact]
public void Reconcile_SmallDifference_WithinThreshold()
{
var dbBalance = 5000000m;
var bankBalance = 4999999.99m;
var result = Reconciler.Check(dbBalance, bankBalance);
Assert.True(result.IsReconciled);
}
[Fact]
public void Reconcile_LargeDifference_FlaggedAsCritical()
{
var dbBalance = 5000000m;
var bankBalance = 4500000m;
var result = Reconciler.Check(dbBalance, bankBalance);
Assert.False(result.IsReconciled);
Assert.Equal(AlertSeverity.Critical, result.AlertLevel);
}
}
Critical Test Scenarios
| Scenario | Expected Behavior | Priority |
|---|---|---|
| Double investment attempt (idempotency) | Second attempt returns existing investment, no duplicate | Critical |
| EMI collected but investor payout fails | Retry payout, maintain EMI as collected, alert ops | Critical |
| Escrow balance goes negative | Prevented by balance check, transaction rejected | Critical |
| Credit bureau API timeout | Queue for retry, do not approve loan without bureau data | Critical |
| Concurrent investment in same loan | Pessimistic lock prevents overselling | High |
| NACH file generation with holidays | Skip holiday dates, adjust collection calendar | High |
| Prepayment mid-month | Calculate exact interest up to prepayment date, adjust EMI | High |
| Investor sells loan part mid-EMI cycle | Current EMI goes to seller, next EMI to buyer | High |
25. Interview Q&A
Q1: How do you handle the case where a borrower pays early (prepayment) and the loan is partially funded by multiple investors?
When a borrower prepays, the outstanding principal must be reduced proportionally across all investors. The system calculates the exact principal owed to each investor based on their investment share, processes the prepayment through the escrow account, credits each investor's wallet, and adjusts the remaining EMI schedule using a reducing balance method. The key challenge is that the EMI schedule must be recalculated in real-time, and investors must be notified of the changed cash flow projections on their dashboard. The prepayment must also update the credit bureau record to reflect the reduced outstanding amount.
Q2: How do you ensure that investor funds in escrow are never mixed with platform funds?
The escrow account is maintained with a scheduled commercial bank as a trust account separate from the platform's operating bank account. All investor deposits go directly into the escrow account, and disbursements to borrowers originate from the escrow account. The platform never has direct access to the escrow funds. The escrow trustee (bank) processes instructions from the platform but only for valid loan disbursements and investor withdrawals. A daily reconciliation compares the platform's internal ledger with the bank's escrow statement, and any discrepancy triggers an immediate alert to the compliance team and the RBI reporting officer.
Q3: What happens if the platform goes down during a batch repayment processing cycle?
The batch repayment processing is designed to be idempotent and resumable. Each NACH file submission is tracked with a unique batch ID, and each individual debit is tracked with an idempotency key. If the system crashes mid-batch, on restart it checks the status of all submitted debits via the NACH gateway API, processes the results for completed debits, and resubmits only the debits that were not confirmed. No EMI is charged twice because each debit submission includes the idempotency key. The recovery process takes at most 30 minutes and requires no manual intervention.
Q4: How do you handle the scenario where an investor tries to invest more than the RBI-permitted limit for a specific borrower?
The compliance check runs synchronously during the investment flow. Before accepting any investment, the system queries the credit bureau to check the investor's total exposure to that borrower across all P2P platforms. If the investment would push the investor over the 10 lakh limit, the system rejects the transaction with a clear error message. For auto-invest rules, the compliance check is built into the rule evaluation engine so that no auto-investment can breach the limit. The system maintains an internal cache of exposure data that is refreshed daily from the bureau, and any investment above 8 lakh triggers a real-time bureau check for the most current data.
Q5: Design the data model for tracking every dollar in the system with full audit trail
Every financial event creates an immutable entry in the ledger table with a sequential transaction ID, timestamp, account IDs involved, debit amount, credit amount, running balance, and a SHA-256 hash of the previous entry (similar to blockchain). The ledger cannot be updated or deleted, only appended. A background job runs hourly to recompute balances from the ledger and compare with the current balance table. Any discrepancy halts new transactions on the affected account and alerts the operations team. The ledger is stored in a separate database with restricted access so that even application administrators cannot modify historical entries.
Q6: How would you handle a situation where the credit bureau API is down and a borrower needs a loan urgently?
The credit bureau API has a 99.9% SLA, but outages do happen. The system implements a multi-layer resilience strategy. First, we maintain a local cache of the borrower's last bureau pull (refreshed every 30 days). If the cache is fresh enough, we use it. If the cache is stale, we have a secondary bureau provider (CRIF or Experian) as a fallback. If both bureaus are down, we enter a degraded mode where loans above 2 lakh are queued for manual review and loans below 2 lakh use the platform's internal scoring model based on alternative data only. No loan is approved without at least one credit assessment source, and all degraded-mode approvals are flagged for retrospective bureau verification when the API recovers.
Q7: Explain the difference between NACH auto-debit failure due to insufficient funds vs technical failure. How does the system handle each?
Insufficient funds means the borrower's bank account does not have enough balance to cover the EMI. Technical failure means the NACH system itself had an issue (network error, bank gateway timeout, file format error). For insufficient funds, the system schedules automatic retries on Day 3, Day 7, and Day 14, sends progressively urgent SMS and email reminders, and starts the dunning process if all retries fail. For technical failures, the system resubmits the same debit instruction on the next business day without any impact on the borrower's record or notification, since the failure is not the borrower's fault. The distinction is critical because treating a technical failure as a borrower default would be incorrect and potentially violate fair lending practices.
Q8: How do you calculate XIRR for an investor's portfolio when they have made partial investments and received partial repayments?
XIRR (Extended Internal Rate of Return) is calculated by finding the discount rate that makes the net present value of all cash flows equal to zero. The cash flows include all investments (negative values representing money going out), all repayments received (positive values), and the current estimated value of outstanding loans (positive). The XIRR calculation uses Newton-Raphson numerical method to solve for the rate. The system must handle edge cases like the investor having only outflows (no repayments yet), very small time differences between transactions, and negative XIRR when defaults exceed returns. The XIRR is recalculated daily and cached for the dashboard, with a background job updating the investor's overall portfolio XIRR and individual loan XIRR.
Q9: Design the system to handle a scenario where RBI changes regulations mid-operation (e.g., reduces interest rate cap from 24% to 18%)
Regulatory changes are managed through a configuration-driven approach. The platform stores regulatory parameters in a versioned configuration table with effective dates. When the RBI announces a rate cap change, the compliance team updates the configuration with the new rate and the effective date. New loans immediately apply the new cap. Existing loans continue under their original terms since the change is not retroactive. The system generates a report of all loans that would have been affected if applied retroactively, which the compliance team uses for regulatory correspondence. The configuration change also triggers a recalculation of all auto-invest rules to ensure they remain valid under the new regulations.
Q10: How do you prevent a single point of failure in the payment collection system?
The payment collection system is designed with multiple layers of redundancy. The NACH gateway has a primary and backup provider. The UPI mandate system is an alternative collection channel if NACH is unavailable. The batch processor runs on a dedicated server with automatic failover to a standby instance. If both the primary and backup NACH providers are down, the system can generate manual payment links via UPI for borrowers to self-pay. The escrow reconciliation runs independently of the collection pipeline, so a collection failure does not affect fund safety. The entire collection pipeline is idempotent, meaning it can be safely restarted from any point without duplicating transactions.
Q11: Explain how you would handle a scenario where two investors try to auto-invest in the same loan simultaneously and the loan only has capacity for one more investment.
This is a classic race condition. The solution uses distributed locking. When the auto-invest engine processes a loan, it acquires a Redis-based distributed lock on the loan ID. The lock has a 30-second TTL to prevent deadlocks. The first engine instance to acquire the lock processes all matching rules for that loan. The second instance, when it tries to acquire the lock, either waits for the lock to be released (with a timeout) or skips the loan if it's already fully funded. Within the lock, the system re-reads the remaining funding amount from the database to ensure it has not changed since the lock was acquired. This optimistic-within-pessimistic approach prevents both double-spending and deadlocks.
Q12: How do you handle platform fees when the investor's share of EMI includes both principal and interest?
The platform fee is charged only on the interest component, not on the principal. When an EMI is collected, the system splits it into principal and interest parts based on the amortization schedule. The platform fee is calculated as a percentage of the interest part (e.g., 1% of interest). The net investor payout is (principal share + interest share - platform fee). The platform fee is accumulated in a separate ledger account and settled monthly to the platform's operating account through a scheduled transfer. The investor's tax statement (Form 26AS integration) reflects the gross interest earned before platform fees, since the platform fee is the investor's expense, not income.
Q13: Describe how you would migrate a running P2P lending platform from a monolithic architecture to microservices without any downtime.
The migration follows the Strangler Fig pattern. We identify bounded contexts within the monolith and extract them one at a time. First, we add an API gateway in front of the monolith that routes requests. Then we extract the least critical service (like notification) into its own microservice, with the gateway routing notification endpoints to the new service and everything else to the monolith. We use the strangler proxy to progressively route more endpoints. Database migration uses the outbox pattern: both the monolith and the new service write to the same database initially, and a change data capture pipeline syncs data to the new service's database. Once the new service is stable, we switch reads to the new database and decommission the old code path. Each migration step can be rolled back independently. The entire migration takes 6 to 9 months for a platform of this complexity.
Q14: How would you implement real-time fraud detection without adding latency to the loan application flow?
The fraud detection system operates in two modes: synchronous pre-screening and asynchronous deep analysis. During loan application, a lightweight rule engine (running in-memory with Redis) checks approximately 20 rules in under 50 milliseconds. These rules include duplicate PAN/Aadhaar, device fingerprint velocity, IP reputation, and basic income consistency checks. If the pre-screening passes, the application proceeds immediately while a background job performs deeper analysis including graph-based collusion detection, bank statement anomaly analysis, and cross-platform exposure checks. If the deep analysis flags the application after it has been approved, the loan disbursement is frozen and the compliance team is notified. This approach keeps the application latency low while catching sophisticated fraud that simple rules cannot detect.
Q15: What are the key metrics you would monitor to detect platform health issues before they impact users?
Infrastructure metrics include CPU utilization above 80% for 5 minutes, memory usage above 85%, disk IOPS saturation, and network latency between services above 10ms. Application metrics include API response time P99 above 500ms, error rate above 0.1%, queue depth above 1000 messages, and job execution time exceeding SLA. Business metrics include default rate trending upward for 3 consecutive days, collection efficiency dropping below 95%, KYC approval time exceeding 24 hours, loan funding time exceeding 72 hours, and investor withdrawal processing time exceeding 4 hours. Each metric has three alert levels: warning (page the on-call engineer), critical (page the engineering lead), and emergency (page the CTO and trigger incident response). All alerts must be actionable and include a runbook link with resolution steps.
Conclusion
Designing a P2P lending platform is a comprehensive exercise that spans the full spectrum of system design challenges: from credit assessment and risk modeling to escrow fund management and regulatory compliance, from real-time auction mechanisms to batch repayment processing, from fraud detection to investor dashboards. The platform sits at the intersection of technology, finance, and regulation, requiring engineers to think not just about scalability and performance, but also about correctness, auditability, and compliance.
The key takeaways from this design are: financial data requires strong consistency and cannot tolerate eventual consistency for balances or transaction records, regulatory compliance must be embedded into the system architecture rather than bolted on as an afterthought, the escrow system is the trust backbone of the platform and must be implemented with zero tolerance for discrepancies, credit assessment accuracy directly impacts investor returns and platform reputation, and the system must be designed for resilience because financial transactions cannot be interrupted by infrastructure failures.
In a system design interview, the P2P lending platform question gives you the opportunity to demonstrate depth in multiple domains. Start with the core lending lifecycle (application, assessment, listing, funding, disbursement, repayment), then expand into the supporting systems (KYC, credit scoring, escrow, notifications), and finally discuss the operational aspects (compliance, monitoring, testing). The candidate who can navigate between high-level architecture and low-level implementation details while keeping the regulatory context in mind will stand out in any senior or staff-level interview.