Engineering a Loan Origination Pipeline
Design a loan origination pipeline from application through underwriting, offer, and disbursal.
A loan origination system (LOS) is the machine that turns a stranger with a form into a booked loan on a ledger. It sits between a marketing funnel and a servicing platform, and its job is deceptively narrow: decide who to lend to, on what terms, and move money exactly once. Everything hard about the LOS lives in the seams between those steps — where a stateless web form meets a stateful decision, where a soft credit check becomes a hard one, where an approval becomes an irreversible disbursement.
The pipeline reads as a straight line, but each stage owns a different failure mode. Here is the shape of it end to end.
Application ──▶ KYC + Credit Pull ──▶ Underwriting ──▶ Offer
│ │
(decline) Accept
│ │
Adverse Disbursal ──▶ Booked Loan
Action
▸ Open the interactive diagram — pan, zoom, and trace every step (light/dark, self-contained).
The rest of this piece walks each stage as an engineering surface: what state it holds, what it calls out to, and what has to be true before the next stage can run.
Application intake is a state machine, not a form
The temptation is to treat intake as a single POST that validates a payload and writes a row. In practice an application is a long-lived, resumable object. Applicants abandon and return days later; documents arrive out of band; a co-applicant is added after the first submission. Model the application as an entity with an explicit status (draft, submitted, in_review, offered, accepted, funded, declined, withdrawn) and treat every field change as an event appended to that entity.
Two design choices pay off immediately. First, assign the application ID on the very first interaction and carry it through every downstream call — it becomes the idempotency and correlation key for the entire pipeline. Second, separate captured data (what the applicant typed) from derived data (what you computed or fetched). When a bureau report or an income estimate changes, you want to know whether the applicant lied or your enrichment drifted.
Validation at intake should be cheap and structural: is the SSN well-formed, is the requested amount inside product bounds, is the applicant of age in this jurisdiction. Save the expensive, external checks for the next stage, where you can run them once against a stable snapshot.
KYC and the credit pull: the first external dependencies
This is the first stage that leaves your walls. It typically fans out to three classes of provider: identity verification (document plus liveness, or a data-only KYC vendor), sanctions and PEP screening, and a credit bureau pull. Each is slow, rate-limited, occasionally down, and — critically — sometimes billable per call.
Because these calls cost money and have side effects, wrap them in an idempotent, cached layer keyed by the application ID and a request hash. A retry after a network timeout must not trigger a second hard inquiry on the applicant’s credit file; that is both a cost bug and a compliance problem. The distinction between a soft pull (no impact on the applicant’s score, used for pre-qualification) and a hard pull (recorded, used at the point of a real offer) is a business rule you encode explicitly, not an accident of which endpoint you happened to call.
Treat the outputs as a normalized decision snapshot:
snapshot = {
"application_id": app.id,
"kyc": {"status": "verified", "score": 0.94},
"sanctions": {"hit": False},
"bureau": {"score": 712, "inquiries_6mo": 2, "utilization": 0.31},
"captured_income": 84000,
"pulled_at": "2026-08-18T14:02:00Z",
}
Everything the underwriter decides on should come from this frozen snapshot, not from live re-fetching. That makes decisions reproducible and auditable: given the same snapshot, the engine must always reach the same verdict.
Underwriting: rules, scores, and a defensible verdict
Underwriting is where the LOS earns its keep, and where regulated lending diverges hardest from generic software. The engine consumes the snapshot and emits a decision: approve, decline, or refer to a human. Whatever internal model you use, the output must be explainable. If you decline, most jurisdictions require you to state the principal reasons — the adverse-action reasons — in terms a person can understand.
That constraint shapes the architecture. Keep the policy layer declarative and versioned, separate from the scoring model. A rule set might read as a cascade of hard cutoffs followed by a scored band:
if sanctions.hit: decline("regulatory")
if bureau.score < 580: decline("credit_score")
if dti(income, debts) > 0.5: decline("debt_to_income")
if bureau.score < 660: refer("manual_review")
else: approve(pricing_tier(bureau.score))
Version every policy set and stamp each decision with the policy version that produced it. When a regulator or an internal auditor asks why an application from six months ago was declined, you replay the exact snapshot against the exact policy version and get the exact same answer. A model that cannot be replayed is a liability, not an asset.
The refer path matters as much as approve and decline. Real portfolios have edge cases — thin files, recent immigrants, self-employed income — that a rule set should escalate rather than reject. Build the manual-review queue as a first-class state, not a dumping ground.
Offer, acceptance, and the point of no return
An approval is not yet an offer. The offer stage converts a decision into concrete terms — principal, APR, term length, fees, repayment schedule — and presents them for acceptance. This is also where a soft pre-qualification typically converts to the hard credit pull, because you are now making a firm, binding offer.
Acceptance is a legal event. The applicant e-signs a disclosure and a note; you capture consent with a timestamp, the document version shown, and enough context to reconstruct exactly what they agreed to. Store the rendered disclosure, not just a template ID — regulated language changes, and you need the words the applicant actually saw.
The transition from accepted to funded is the single most dangerous edge in the system. Everything before it is reversible; disbursement is not. Guard it with a hard idempotency key and a two-phase pattern: reserve the funding intent, confirm no competing disbursement exists for this application, then execute. A double-disbursal is far worse than a delayed one, so the safe failure mode is always “stop and alert,” never “retry blindly.”
Booking, reconciliation, and the handoff
Once funds move, the loan is booked: written to the ledger as a real asset with an amortization schedule, a first payment date, and an account the servicing platform can act on. Booking should be driven off a confirmed disbursement event, not off the acceptance — you book what actually funded, at the amount that actually left the account.
Two operational habits keep the tail clean. Reconcile disbursements against the money-movement rail daily; a payment that shows funded in your system but never cleared the rail is a break you want to catch in hours, not at month-end. And treat the handoff to servicing as an explicit contract: the servicing platform receives an immutable booking record, and any later change (a payoff, a modification, a charge-off) is servicing’s story to tell, not the LOS’s.
The pipeline looks linear because it is designed to. The engineering value is in making each transition idempotent, each decision reproducible, and the one irreversible step — moving money — impossible to trigger twice. Get those three properties right and the LOS becomes boring in the best way: a machine that turns applications into loans exactly once, and can always explain why.