Digital Lending Jounery with all retail loan type
- Anand Nerurkar
- Sep 13
- 14 min read
Digital Lending – Unified Architecture Flow (Text Version)
1. Entry Points
Angular Frontend App
Loan type selection (Personal, Auto, Home, BNPL, Micro)
Captures user inputs → Sends REST/GraphQL request to Loan Origination API Gateway
API Gateway (Azure API Management)
AuthN via Azure AD / OAuth2
Routes to respective Loan Orchestration Service
2. Core Microservices (Deployed on AKS)
Loan Orchestration Service
Publishes loan.initiated event to Kafka with:
Loan Type
Customer ID
Correlation ID
Idempotency Key
KYC Service
Consumes loan.initiated
Performs eKYC (Aadhaar/PAN/DigiLocker)
Publishes kyc.completed
Credit Score Service
Consumes kyc.completed
Calls bureau / internal scorecard
Publishes creditscore.completed
Collateral Service (only for Auto/Home)
Consumes creditscore.completed (if loan type requires collateral)
Triggers:
Vehicle valuation + RTO lien marking (Auto)
Property valuation + legal verification (Home)
Publishes collateral.verified
Risk/Underwriting Service
Listens to creditscore.completed and collateral.verified
Applies rules engine (Drools/Decision Table)
Publishes loan.approved or loan.rejected
Agreement Service
Consumes loan.approved
Generates sanction letter & e-agreement
Publishes agreement.signed
Disbursement Service
Consumes agreement.signed
Triggers payment through Core Banking / Payment API
Publishes loan.disbursed
3. Kafka Event Topics
loan.initiated
kyc.completed
creditscore.completed
collateral.verified
loan.approved
loan.rejected
agreement.signed
loan.disbursed
loan.failed (for saga rollback scenarios)
4. Branching by Loan Type
Personal Loan
loan.initiated → kyc.completed → creditscore.completed →
loan.approved → agreement.signed → loan.disbursed
No collateral check
Fully automated underwriting
BNPL / Consumer Durable Loan
loan.initiated → kyc.completed (OTP only) → lightweight score →
loan.approved → agreement.signed → loan.disbursed (merchant settlement)
Low friction, minimal KYC
Instant decisioning, sub-second SLAs
Auto Loan
loan.initiated → kyc.completed → creditscore.completed →
collateral.verified (vehicle validation + lien marking) →
loan.approved → agreement.signed → loan.disbursed (dealer)
Adds vehicle data capture + lien step
Disbursement only after lien marking confirmed
Home Loan
loan.initiated → kyc.completed → creditscore.completed →
collateral.verified (property valuation + legal check) →
manual underwriting approval →
loan.approved → agreement.signed →
loan.disbursed (tranches if construction linked)
Multi-step manual approvals
Collateral check may involve external legal vendor integration
Disbursement can be partial (multiple loan.disbursed events)
Micro Loans
loan.initiated → kyc.completed (alternate data) →
alt.credit.score.completed → loan.approved →
agreement.signed → loan.disbursed
Uses alternate data (UPI txn, mobile data, etc.)
Real-time scoring models (ML-based)
5. Saga / Compensation Flow
If any service fails:
loan.failed event is published
Orchestration service triggers rollback:
Reverse disbursement (if money already sent)
Cancel lien (Auto)
Void sanction letter (Home/Personal)
6. Cross-Cutting Concerns
Correlation ID + Idempotency Key generated at loan.initiated
Redis Cache to store intermediate state for idempotency & fast reads
Cosmos DB / Azure SQL stores loan application, events, decision logs
Azure Monitor + Grafana for observability
Security: Azure AD + API Gateway throttling + encrypted payloads (PII)
✅ Key Advantage:This design is loan-type agnostic — same set of microservices are used, but behavior changes via:
Product Config Table (decides whether collateral step required)
Business Rules (auto vs manual underwriting)
Workflow Orchestration (skips/branches steps)
Home Loan Journey
===
🏠 Home Loan – Detailed Event-Driven Journey (Text Version)
1. Loan Initiation
Actor: Customer (via Angular Frontend)
Flow:
Customer fills loan application (loan amount, property details)
API Gateway (Azure API Mgmt) forwards request to Loan Orchestration Service
Loan Orchestration Service
Generates Correlation ID + Idempotency Key
Stores initial application in Cosmos DB
Publishes event:loan.initiated {loanType=HOME, correlationId, customerId, propertyDetails}
2. KYC Step
KYC Service
Consumes loan.initiated
Performs full eKYC (PAN + Aadhaar + Address Proof via DigiLocker)
Updates application status in DB
Publishes:kyc.completed {correlationId, kycStatus=PASS}
3. Credit Score + Affordability
Credit Score Service
Consumes kyc.completed
Calls Bureau API + internal affordability model (DTI, FOIR)
Persists score + decision data
Publishes:creditscore.completed {correlationId, cibilScore, dti, eligibility}
4. Collateral Verification (Property Validation)
Collateral Service
Consumes creditscore.completed
Triggers two external vendor workflows:
Property Valuation Vendor API → receives valuation report
Legal Vendor API → performs title search, encumbrance check
Both results stored in DB
Publishes:collateral.verified {correlationId, valuationReportId, legalClearanceStatus}
5. Multi-Step Manual Approval
Risk/Underwriting Service
Waits for both creditscore.completed & collateral.verified (Kafka Join)
Generates Underwriting Case in workflow queue (Camunda / Azure Logic App)
Manual approvers are notified (Credit Officer, Risk Head)
Steps:
Step 1: Credit Officer reviews & approves
Publishes: manual.approval.level1.completed
Step 2: Risk Head final approval
Publishes: manual.approval.level2.completed
Once all levels approved:
Publishes:loan.approved {correlationId, sanctionAmount, approvedTenor}
6. Agreement Generation
Agreement Service
Consumes loan.approved
Generates sanction letter & loan agreement PDF
Sends for e-sign (AADHAAR eSign / NSDL)
After successful sign:
Publishes:agreement.signed {correlationId, agreementDocId}
7. Partial / Tranche Disbursement
Disbursement Service
Consumes agreement.signed
Checks property construction stage from external vendor / builder API
Disburses first tranche (e.g., 20% of loan)
Publishes:loan.disbursed {correlationId, tranche=1, amount=20%}
Subsequent Tranches:
Triggered by builder demand letter upload / stage completion
Loan Orchestration Service republishes:disbursement.requested {correlationId, tranche=N}
Disbursement Service validates stage, releases funds
Publishes multiple events until:loan.disbursed {correlationId, tranche=FINAL, amount=remaining}
8. Saga / Rollback (if failure)
If legal clearance fails → loan.rejected
If customer drops application post-approval → publishes loan.cancelled
If partial disbursement already made → triggers compensation workflow (recall funds, notify risk)
9. Observability & Compliance
Audit Service subscribes to all events (loan.*) → stores in immutable audit store
Regulatory Reporting Service generates FIU-IND & RBI compliance reports
Complete Home Loan Event Sequence
loan.initiated →
kyc.completed →
creditscore.completed →
collateral.verified →
manual.approval.level1.completed →
manual.approval.level2.completed →
loan.approved →
agreement.signed →
loan.disbursed (tranche=1) →
loan.disbursed (tranche=2) →
...
loan.disbursed (tranche=FINAL)
Key Points
✅ External Vendor Integration: property valuation & legal checks done asynchronously → no UI blocking
✅ Multi-Level Manual Approvals: handled via workflow engine with human tasks
✅ Partial Disbursement: multiple loan.disbursed events handled idempotently
✅ Auditability: every event stored for compliance (RBI/SEBI)
✅ Saga Rollbacks: prevent partial exposure risk if legal/credit fails mid-way
✅ Yes — you are absolutely correct (and that is a better, production-grade approach).Let’s refine the design to reflect a transactional write + event outbox pattern instead of writing directly to Cosmos DB.
Here’s how it should work:
Corrected Data Flow – Home Loan (and Other Loan Types)
1. Loan Initiation – Data Write
Loan Orchestration Service
Accepts application request from API Gateway
Starts a DB transaction on Azure Database for PostgreSQL
Writes:
Loan application master record
Customer details (if new)
Writes an outbox event (loan.initiated) in the same transaction✅ This guarantees atomicity (no loan without event / no event without loan)
2. Outbox → Kafka Publish
Outbox Event Publisher (Debezium / Custom Scheduler)
Reads unprocessed outbox events from Postgres
Publishes them to Kafka topic loan.initiated
Marks them as published in Postgres (idempotency-safe)
3. Event Consumers Update Caches + NoSQL
Cache Consumer
Consumes loan.initiated (and subsequent events)
Updates Redis cache for low-latency loan lookup
NoSQL Consumer
Consumes loan.initiated (and subsequent events)
Writes denormalized document into Cosmos DB
Purpose: fast query for UI dashboards, audit, analytics
Acts as a read replica of Postgres + derived events
4. Subsequent Steps
All other services (KYC, Credit Score, Collateral, etc.) consume Kafka events, perform their work, and write:
DB updates (Postgres → Outbox)
Next event (via outbox → Kafka)
Cache + NoSQL get updated continuously to reflect the latest loan state
Benefits of This Approach
Area | Direct Cosmos Write | Postgres + Outbox Pattern (Recommended) |
Consistency | Eventual, can have drift | Strong — DB + event commit together |
Transaction Safety | Risk of partial writes (DB vs Cosmos mismatch) | Atomic commit ensures no data loss |
Idempotency | Manual deduplication needed | Outbox handles dedupe + replay |
Cache Population | Manual triggers needed | Event-driven (auto updated) |
Auditability | Harder (multiple DBs) | Easy — Postgres is source of truth |
Corrected Initial Step in Home Loan Journey
POST /loan-application
↓
Loan Orchestration Service
- Start TX in Postgres
- Insert LoanApplication (status=INITIATED)
- Insert OutboxEvent(loan.initiated)
- Commit
↓
OutboxPublisher picks event → Kafka
↓
CacheConsumer → Redis
NoSQLConsumer → Cosmos DB
↓
KYC Service consumes event → next step
So yes — Cosmos DB is not the primary store, but a projection / query-optimized replica populated asynchronously.This is the CQRS + Outbox pattern, which is the industry best practice for a microservices-based lending platform.
1 — Canonical technical assumptions (applies to all flows)
Frontend: Angular SPA (Customer / Dealer / Agent) → calls API Gateway (APIM).
API Gateway: Authentication (Azure AD B2C), request validation, rate limiting; forwards to Loan Orchestration Service.
Source of truth DB: Azure Database for PostgreSQL (transactional).
Outbox: outbox_event table in Postgres written in the same transaction as application changes.
Outbox publisher: Debezium or a scheduled publisher reads outbox rows and publishes to Kafka, marks as published.
Event bus: Kafka (topics per domain event).
Consumers:
CacheConsumer → updates Redis (fast reads).
NoSQLConsumer → writes denormalized documents into Cosmos DB (read-model / UI).
Domain microservice consumers (KYC, Score, Collateral, Underwriting, etc.).
Microservices: Spring Boot services deployed on AKS, each owns its DB/aggregate or uses Postgres with proper aggregate boundaries.
Correlation & Idempotency:
correlation_id included in all events/DB rows.
idempotency_key required on client POSTs; services ensure idempotent handling.
Sagas / Compensation: Each step that changes external systems emits compensating events (example: disbursement.rollback.requested).
Manual tasks: Workflow engine (Temporal/Camunda) or workbench for human approvals; manual steps produce events when completed.
2 — Core domain events (canonical list)
(Version all event names, payloads should include correlation_id, app_id, customer_id, loan_type.)
loan.initiated
loan.application.persisted (outbox transitional)
document.uploaded
kyc.completed
creditscore.completed
eligibility.decided
collateral.verified
valuation.reported
legal.clearance.completed
manual.approval.requested
manual.approval.completed
loan.approved
agreement.generated
agreement.signed
disbursement.requested
loan.disbursed (include tranche number)
repayment.schedule.created
repayment.received
loan.rejected
loan.cancelled
compensation.requested
audit.event (for immutable audit store)
3 — Database + outbox pattern (text)
Client (Angular) POSTs /v1/loan-applications with Idempotency-Key.
Loan Orchestration Service:
Begin Postgres TX.
Insert into loan_application (status = INITIATED) — primary record.
Insert into outbox_event (event_type=loan.initiated, payload with app data).
Commit TX.
OutboxPublisher reads outbox row, publishes loan.initiated to Kafka, updates outbox_event.published=true.
Consumers (CacheConsumer, NoSQLConsumer, KYC, Score) pick up loan.initiated and proceed.
4 — Projection consumers (text)
CacheConsumer:
Consumes every loan.* event.
Updates Redis keys for quick UI read: loan:{appId}:status, loan:{appId}:nextStep, loan:{appId}:latestEvent.
TTLs for non-critical items, or immediate deletion on closure.
NoSQLConsumer (Cosmos DB):
Consumes every loan.* event.
Writes denormalized document loan_document combining application + latest events + collateral summary + KYC status for fast UI queries and analytics.
5 — Orchestrator behaviour (how product config is used)
Product Configuration Service (Postgres table product_config) contains:
product_code (PERSONAL, BNPL, AUTO, HOME, MICRO)
steps (ordered list of step ids)
flags (requires_collateral, requires_valuation, requires_legal, requires_manual_approval)
auto_approval_thresholds (score, ltv, dti)
disbursement_mode (LUMP_SUM, TRANCHE)
manual_approval_flow (levels & approvers)
sla_seconds_per_step
On loan.initiated the Loan Orchestration Service:
Reads product_config for product_code.
Creates the workflow instance (lightweight state record in Postgres workflow_instance) with the steps sequence and current pointer.
Emits initial domain event(s) corresponding to first step(s), e.g. document.uploaded if docs were included, else kyc.started / kyc.requested.
Workflow state is advanced by upstream events (e.g., when kyc.completed arrives, orchestrator marks step completed and triggers next event(s) based on config).
If steps include parallelism (KYC + Score) orchestrator can emit both initial requests or let consumers act on loan.initiated.
6 — Business rules engine integration (text)
Rules Engine options: Drools / Decision Tables / DMN, or embedded rules in Eligibility Service.
Where rules live:
Static rules (product thresholds) in product_config.
Dynamic rules (complex underwriting) in Drools knowledge base; rules are versioned and deployed separately.
Runtime:
Underwriting Service calls Rules Engine with input facts: cibilScore, dti, ltv, incomeDocsVerified, legalClearance.
Rules evaluate and output decision: AUTO_ACCEPT, REFER_MANUAL, AUTO_REJECT, plus sanctionAmount, interestRate.
Rule-driven branching:
If result AUTO_ACCEPT → Underwriting publishes loan.approved.
If REFER_MANUAL → Underwriting publishes manual.approval.requested (with required approvers & SLA).
If AUTO_REJECT → Underwriting publishes loan.rejected (reason codes).
7 — Concrete sequences (text) — all loan types
For each sequence I show: UI action → Postgres+Outbox → Kafka → Consumers (Cache, NoSQL) → Domain services → subsequent events → final disbursement/repayment.
A) Personal Loan (fastest path — no collateral)
UI → POST /v1/loan-applications (body includes product_code=PERSONAL) with Idempotency-Key.
Loan Orchestration Service: write loan_application, write outbox_event (loan.initiated); commit.
OutboxPublisher → publish loan.initiated to Kafka.
CacheConsumer → Redis; NoSQLConsumer → Cosmos DB (application snapshot).
KYC Service consumes loan.initiated:
performs eKYC (Aadhaar/PAN)
writes KYC result (Postgres) + outbox kyc.completed.
Credit Score Service consumes kyc.completed:
calls bureau & internal models
writes result + outbox creditscore.completed.
Eligibility Service consumes creditscore.completed (and kyc.completed):
fetches product_config(PERSONAL) (flags: no collateral, auto approval thresholds)
runs rules → decides AUTO_ACCEPT if score >= threshold
writes eligibility.decided event via outbox.
Underwriting Service consumes eligibility.decided:
If auto → publishes loan.approved via outbox.
Else publishes manual.approval.requested.
Agreement Service consumes loan.approved:
generates agreement, triggers eSign → on sign publishes agreement.signed.
Disbursement Service consumes agreement.signed:
initiates payment via Payment Connector / CBS → on success publishes loan.disbursed (tranche=1).
Repayment Service creates schedule on loan.approved or agreement.signed event and publishes repayment.schedule.created.
UI queries read-model (Cosmos or Redis) for status updates.
Key notes:
All writes to Postgres use outbox for event emission.
Cache & NoSQL projections ensure UI remains snappy.
B) BNPL / Consumer Durable (merchant-embedded, low friction)
Merchant Portal / UI triggers loan.initiated with product_code=BNPL and merchant metadata.
Loan Application persisted w/ outbox → loan.initiated.
Outbox → Kafka → Cache/NoSQL updates.
Lightweight KYC step: kyc.completed may be OTP-only; sometimes skipped depending on merchant-level trust.
Internal lightweight scoring service consumes (uses internal risk model, historical merchant settlement data) → creditscore.completed or altscore.completed.
Eligibility Service uses product_config(BNPL) (low thresholds, instant approval policies) → typically AUTO_ACCEPT.
Agreement generated (simplified) and agreement.signed emitted (may be implicit by merchant T&C acceptance).
Disbursement to Merchant Settlement account via loan.disbursed event; settlement engine batches/clears later.
Collections: Merchant settlement reconciles transactions; platform collects from borrower via scheduled EMIs/UPI.
Key differences:
Merchant context included in events: merchant_id, order_id.
Disbursement mode usually LUMP_SUM to merchant (or split by merchant settlement terms).
High-volume, low-latency processing; aggressive caching and partitioning.
C) Auto Loan (collateral + RTO/hypothecation)
UI (Dealer/Customer) POSTs loan.initiated with product_code=AUTO + vehicle details.
Postgres + Outbox → loan.initiated.
Outbox → Kafka → Cache & Cosmos.
KYC & Credit Score proceed as in Personal flow (kyc.completed, creditscore.completed).
Collateral Service consumes creditscore.completed (or loan.initiated depending on config) and:
Validates vehicle details (VIN, chassis, engine) via Dealer / RTO connector → valuation.reported with market value.
Calculates LTV = requestedAmount / valuation.
If hypothecation required: triggers Hypothecation Service → calls RTO for lien registration.
On successful registration publishes collateral.verified (include lien_registration_id).
Eligibility Service receives creditscore.completed + collateral.verified:
Runs rules: checks LTV <= product_config.auto.ltv_threshold; DTI; other checks.
Output: eligibility.decided.
Underwriting executes business rules (may AUTO_ACCEPT or REFER_MANUAL).
If accepted: Agreement Service produces lender & hypothecation docs → eSign & agreement.signed.
Disbursement Service waits for collateral.verified & agreement.signed before issuing disbursement.requested → pays dealer (NEFT) → emits loan.disbursed.
Any failure in lien registration → loan.rejected or compensation.requested if funds already moved.
Key differences:
Collateral & lien registration precondition for disbursement.
Extra external connectors: Dealer/DMS, RTO.
Compensation logic must handle partial external state (registered lien vs not).
D) Home Loan (multi-step manual approvals + external vendors + tranche disbursement) — FULL DETAILED SEQUENCE
(This is the full requested detailed flow — long-running, many external interactions.)
Step 0 — Client submission
Customer submits home loan application with property metadata + document uploads (title docs, sale deed, NOC, builder letters).
Loan Orchestration Service:
Begin TX in Postgres.
Insert loan_application (status=INITIATED).
Insert outbox_event (loan.initiated).
Commit.
Step 1 — Outbox → Initial projections
OutboxPublisher publishes loan.initiated to Kafka.
CacheConsumer updates Redis; NoSQLConsumer writes denormalized doc to Cosmos.
Step 2 — Document ingestion & OCR
Document Service consumes loan.initiated and:
Stores uploads in Blob Storage.
Calls Form Recognizer/OCR asynchronously.
On OCR completion publishes document.uploaded events with document_id.
NoSQLConsumer picks up docs and updates the read model.
Step 3 — KYC & Preliminary checks
KYC Service consumes loan.initiated:
Performs full eKYC (Aadhaar, PAN, CKYC).
Performs AML/PEP screening.
Publishes kyc.completed {status}.
Step 4 — Credit Score & Affordability
Credit Score Service consumes kyc.completed:
Calls bureau & internal affordability engine (DTI, FOIR).
Writes creditscore.completed event with score, affordability metrics.
Step 5 — Collateral: Valuation + Legal (two parallel external vendor flows)
Collateral Service consumes creditscore.completed:
Property Valuation: calls Valuation Vendor API (third-party). Vendor returns valuation.report (market value, vtm).
Legal Title Search: calls Legal Vendor API. Vendor returns legal.clearance.status (CLEAR / EXCEPTIONS).
Collateral publishes valuation.reported & legal.clearance.completed.
NoSQLConsumer updates read model with valuation & legal summaries.
Step 6 — Orchestration: prepare Underwriting Case
Underwriting Service consumes valuation.reported and legal.clearance.completed + creditscore.completed:
Packages facts into Underwriting Case (cibilScore, valuation, legal exceptions, income verification).
Decides based on Rules Engine:
If AUTO_REJECT → publish loan.rejected.
If AUTO_ACCEPT → publish loan.approved.
If REFER_MANUAL → publish manual.approval.requested with work item details.
Step 7 — Manual multi-level approvals (human workflow)
If manual.approval.requested:
Workflow engine / Workbench creates task(s) assigned per product_config.home.manual_approval_flow (e.g., Credit Officer → Legal Head → Risk Head).
Each approver logs into Workbench (UI) and completes their task:
On completion the Workbench emits manual.approval.completed {level, approverId, decision, comments}.
On final approval (all levels completed with APPROVE) Underwriting publishes loan.approved.
Step 8 — Sanction & Agreement
On loan.approved:
Agreement Service creates sanction letter & loan agreement PDFs.
Agreement Service calls eSign providers (Aadhaar eSign / third-party).
On successful eSign publish agreement.signed.
Step 9 — Pre-disbursement validations
Disbursement Service consumes agreement.signed and:
Reads product_config.home.disbursement_mode (TRANCHE vs LUMP_SUM).
For TRANCHE:
Waits for builder/buyer stage evidence:
builder.stage.completed or customer uploads inspection.report.
Each tranche request is disbursement.requested {trancheNumber, amount, evidenceRef}.
Validates lien/registration status if applicable.
Step 10 — Disbursement (multiple loan.disbursed events)
For each tranche:
Disbursement Service initiates payment via Payment Connector (NEFT/RTGS).
On success publishes loan.disbursed {tranche=N, amount, txnRef}.
Update Postgres loan_account outstanding balance.
NoSQLConsumer updates read-model.
Step 11 — Repayment scheduling & collections
After first tranche (or on loan.approved), Repayment Service creates schedule:
Publish repayment.schedule.created.
Integrate with mandate provider (NACH/UPI) for auto-debit; create mandate.registered events.
Repayment events (repayment.received) update loan_account and may lead to notifications and ledger entries.
Step 12 — Exceptions & compensations
If legal vendor later reports encumbrance (post-disbursement), a legal.exception.raised event is emitted:
Risk team notified; may trigger compensation.requested (recall funds) if severe.
If customer cancels after partial disbursements, compensation flow:
compensation.requested → Disbursement Service tries reversal; if reversal impossible creates manual collection task.
Step 13 — Closing
On full repayment loan.closed event published and read-model updated.
Key home-loan specifics reiterated:
Long-running — workflow instances persisted; events may be days/weeks apart.
External vendor calls are asynchronous; platform does not block UI (use notifications to update applicant).
Tranche-based disbursement means multiple loan.disbursed events with idempotency checks (each tranche has unique trancheId).
Audit: every critical event is recorded in an immutable audit store for compliance.
E) Micro-loans (alternative-data, mobile-first)
UI (mobile) POSTs loan.initiated with product_code=MICRO.
Postgres + Outbox → loan.initiated published.
NoSQL & Cache projections updated.
Alt-Scoring Service consumes event:
Pulls UPI txn history, telco data, device signals (via integration service).
Produces altcreditscore.completed.
Eligibility Service evaluates against product_config.micro (looser thresholds, limits).
Underwriting typically AUTO_ACCEPT.
Agreement implied or eSign via OTP; agreement.signed emitted.
Disbursement instant to bank/UPI → loan.disbursed.
Collections use UPI auto-debit or in-app repayment.
Key differences:
Uses alternate data connectors.
Extremely high throughput; needs optimized partitioning & caching.
8 — Product configuration example (JSON) — how to control flow
Store product configs in product_config table and optionally in a feature-flag service:
{
"product_code": "HOME",
"display_name": "Home Loan - Standard",
"steps": [
"DOCUMENT_UPLOAD",
"KYC",
"CREDIT_SCORE",
"PROPERTY_VALUATION",
"LEGAL_CHECK",
"UNDERWRITING",
"MANUAL_APPROVAL",
"AGREEMENT",
"DISBURSEMENT_TRANCHE"
],
"flags": {
"requires_collateral": true,
"requires_valuation": true,
"requires_legal": true,
"requires_manual_approval": true
},
"auto_approval_thresholds": {
"cibilScore": 750,
"dti": 50,
"ltv": 0.8
},
"disbursement": {
"mode": "TRANCHE",
"tranches": [
{ "percent": 0.2, "condition": "agreement_signed" },
{ "percent": 0.5, "condition": "builder_stage_2" },
{ "percent": 0.3, "condition": "building_completion" }
]
},
"manual_approval_flow": [
{ "level": 1, "role": "CreditOfficer" },
{ "level": 2, "role": "LegalHead" },
{ "level": 3, "role": "RiskHead" }
],
"sla_seconds_per_step": {
"KYC": 3600,
"PROPERTY_VALUATION": 86400,
"LEGAL_CHECK": 172800,
"MANUAL_APPROVAL": 259200
}
}
How it’s used:
Orchestrator reads steps and flags.
Orchestrator creates workflow_instance and issues initial events as needed.
Rules engine uses auto_approval_thresholds for auto-decision.
Disbursement service reads disbursement.tranches to know how many loan.disbursed events to expect and when to allow them.
9 — Rules / Decision Table example (text)
Decision table for UNDERWRITING_DECISION:
Condition: CIBIL >= 750 | DTI <= 50% | LTV <= 80% | Legal Clear | Result |
true | true | true | true | AUTO_ACCEPT |
true | true | true | false | REFER_LEGAL |
false | any | any | any | REFER_MANUAL |
any | >70% | any | any | AUTO_REJECT |
Rules can be authored in Drools and updated via CI/CD with versioning.
10 — Idempotency, retries, error handling (practical)
All endpoints require Idempotency-Key header; the Orchestration Service persists the key and ignores duplicate POSTs.
Outbox ensures events are published exactly-once (or at-least-once with idempotent consumers).
Consumers must be idempotent (check if event already applied using event_id).
Use exponential backoff + DLQ for failed event processing (human review for stuck items).
11 — Compensation examples (text)
Disbursement failed after agreement signed:
disbursement.failed → compensation.requested → Underwriting/Operations notified for manual fix.
Legal exception discovered post-disbursement:
legal.exception.raised → compensation.requested → Risk evaluates, may trigger recall event.
Partial funds reversed require ledger operations and manual.collection.task.
12 — Short operational note for UI developers
The UI should consume from the read-model (Cosmos/Redis) and poll or subscribe to push notifications for status change (websocket).
Each loan card should show currentStep (from read-model) and ETA based on sla_seconds_per_step.
For manual approvals, Workbench UI shows taskId, approverRole, SLA, supportingDocs links (pointing to Blob SAS URLs).
13 — Final summary (one-liner)
Use Postgres + Outbox → Kafka → Cache (Redis) + NoSQL (Cosmos) as the canonical data flow; control per-loan behavior via a product_config + rules engine; orchestration uses workflow instances to sequence and branch steps (parallel & sequential) — this supports all retail loan types including home loan multi-step manual approvals, external vendor property checks, and tranche-based disbursement (multiple loan.disbursed events).
If you want, I can now:
Produce a compact tabular matrix mapping each loan type to required product_config flags & expected events, or
Generate a sample SQL DDL for loan_application, outbox_event, and workflow_instance, or
Provide a Java Spring Boot pseudo-code snippet showing the Postgres transaction + outbox insert + publishing logic.
🏠 Home Loan – New Property vs. Resale Property Journey
1. Common Steps for Both
Loan Initiation → Capture applicant details, property info (builder/project ID or resale property address)
KYC → Aadhaar/PAN + address verification
Credit Score & Affordability Check → Bureau score, income calculation
Underwriting / Risk Assessment → Evaluate eligibility, FOIR/DTI
2. New Property (Approved Project) Flow
If the builder project is pre-approved:
Property Verification Step Is Skipped
No legal opinion needed (already cleared during project approval)
No separate valuation needed (project’s unit pricing is known)
Loan Approval → Agreement → Disbursement
Disbursement typically linked to construction stage
Multiple tranches are still possible (based on builder demand letters)
Event Sequence for New Property:
loan.initiated →
kyc.completed →
creditscore.completed →
loan.approved →
agreement.signed →
loan.disbursed (tranche 1 … tranche N)
3. Resale Property (Secondary Market) Flow
For resale (buying from an existing owner):
Property Verification Step is Mandatory
Property Valuation: Market value vs. sale deed value
Legal Opinion: Encumbrance, title search, chain of ownership
Underwriting Approval waits for property clearance
Agreement → Disbursement
Usually single disbursement at sale deed registration
Event Sequence for Resale Property:
loan.initiated →
kyc.completed →
creditscore.completed →
collateral.verified (valuation + legal opinion) →
manual.approval.completed →
loan.approved →
agreement.signed →
loan.disbursed (one-shot)
4. Product Configuration Example
LoanType | CollateralCheck | ValuationRequired | LegalOpinionRequired | DisbursementMode |
Home (New Project) | FALSE | FALSE | FALSE | MULTIPLE-TRANCHES |
Home (Resale) | TRUE | TRUE | TRUE | SINGLE-SHOT |
Auto | TRUE | TRUE (vehicle valuation) | FALSE (RTO lien check) | SINGLE |
Personal | FALSE | FALSE | FALSE | SINGLE |
BNPL | FALSE | FALSE | FALSE | INSTANT |
Microloan | FALSE | ALT-DATA | FALSE | SINGLE |
5. Business Rule Engine (Drools / DMN Table)
You configure rules like:
if loanType == HOME and propertyType == "NEW" and projectApproved == true:
skipPropertyVerification = true
disbursementMode = MULTIPLE
else if loanType == HOME and propertyType == "RESALE":
skipPropertyVerification = false
requireLegalOpinion = true
disbursementMode = SINGLE
This ensures the same orchestration service dynamically decides which steps to execute.
6. Benefits
✅ Dynamic Workflow: No hardcoding for each loan type
✅ Simplified Maintenance: Add new loan products by just updating configuration + rules
✅ Faster Approvals for New Projects: Skip redundant checks
✅ Full Compliance for Resale: Ensure all legal/valuation steps are done
.png)

Comments