Digital Lending Modernization — End-to-End Case Study (Oracle Forms + PL/SQL → Angular + Spring Boot microservices on Azure)
- Anand Nerurkar
- 3 hours ago
- 12 min read
A fully detailed, interview- and workshop-ready case study you can use as a blueprint for migrating a legacy digital-lending stack (Oracle Forms + PL/SQL) to a modern cloud architecture (Angular SPA, Spring Boot microservices, Kafka streaming, Redis caching) on Azure — including step-by-step phases, tech choices, patterns, artifacts, KPIs, risks and runbooks.
Executive summary
A mid-sized bank runs a Digital Lending product on Oracle Forms + heavy PL/SQL packages. Business requires faster time-to-market, better UX, low latency for real-time decisions, multi-region availability, and regulatory data-residency. We migrate iteratively using the Strangler Fig pattern: extract rules and metadata, expose APIs, reimplement business logic as services, and replace Forms with an Angular frontend. Event streaming (Kafka), Redis caching, and strict security/governance complete the modern platform.
1 — Objectives & success criteria
Functional: Replace Oracle Forms with a responsive Angular app; preserve/verify all business rules and validations.
Non-functional: P95 API latency < 200ms for key read paths; application availability ≥ 99.95%.
Compliance: India user PII stays in India, US user data stays in US (data residency).
Business: Reduce loan decision TAT from hours → <5 minutes for automated paths.
Delivery: Phased migration with minimal disruption; measurable KPIs and rollback plan.
2 — Current state (summary)
Oracle Forms + Reports front-end; PL/SQL packages for validations, calculations and workflows; direct DB access by forms.
Batch processes (Pro*C/PL/SQL) run nightly.
Minimal CI/CD; manual deployments.
No central rule catalog; business logic dispersed in triggers and procedures.
3 — Target architecture (text diagram)
Global Edge
[Azure Front Door / Traffic Manager]
|
[API Gateway (APIM)]
|
-------------------------------
| |
[Angular SPA - region A] [Angular SPA - region B]
| |
Internal App Gateway (ILB) Internal App Gateway (ILB)
| |
[BFF / Backend API] [BFF / Backend API]
| |
-----------------------------------------------
| | | | |
LoanService CustomerSvc CreditSvc RuleSvc PaymentSvc (Spring Boot)
| | | | |
[Local Redis Cache] [Local SQL MI] [Kafka Cluster (regional)]
| | | | |
(Active-Active Redis*) Oracle/Azure SQL MI Kafka Connect -> Data Lake
|
(Private Endpoints only; Hub-Spoke VNets; NSG/Firewall)
* Redis active-active only where regulations permit session/global caches. PII caches remain regional.
4 — High level migration strategy
Philosophy: discover → extract → wrap → replace.
Discover — inventory Forms, triggers, PL/SQL, reports, interfaces, usage metrics.
Extract — run Forms2XML to get form metadata; parse PL/SQL to surface business rules; create a Business Rule Catalog and Decision Tables (DMN where applicable).
Wrap — build thin REST adapters around PL/SQL (temporary) so UI can call APIs instead of Forms directly.
Replace — reimplement logic in microservices (Spring Boot), build Angular UI; switch traffic module-by-module (Strangler Fig).
Optimize — introduce caches, stream processing, SLOs, CI/CD, automated policies.
Decommission — retire Forms and legacy runtime after successful parallel runs.
5 — Phased roadmap (6–12 months suggested)
Phase 0 — Prep (2–4 weeks)
Stakeholder alignment, success metrics, create program backlog, set up Azure landing zone (hub-spoke), identity & security baseline (Azure AD), IaC skeleton (Terraform/Bicep).
Phase 1 — Discovery & Catalog (4–6 weeks)
Forms2XML extraction; PL/SQL inventory; rule extraction and DMN/decision tables; “golden dataset” historic transactions for testing.
Phase 2 — Pilot (6–8 weeks)
Choose low risk but high value module (Customer Profile + KYC).
Build Angular shell, BFF, RuleService (wrap + refactor), temporary REST wrappers for PL/SQL.
CI/CD pipeline + automated tests.
Parallel run & compare outputs.
Phase 3 — Broader Migration (3–4 months)
Migrate Loan Origination (eligibility, credit check, pricing), Payment/Disbursement modules.
Introduce Kafka for events, local Redis for cache, materialized read models for UI.
Phase 4 — Cross-Region & Scale (4–6 weeks)
Add second region (US/UK) with regional stack; implement Traffic Manager geo + nested failover profile; enforce data residency.
Phase 5 — Cutover & Decommission (2–4 weeks)
Full parallel reconciliation, final cutover, retire Forms runtime, perform post-migration optimizations.
6 — Step-by-step activities (detailed)
Phase 1 — Discovery & extraction
Use Forms2XML (or frmcmp with batch) to output XML for each .fmb/.pll. Parse to extract:
UI elements (blocks, items), triggers (WHEN-VALIDATE-ITEM, etc.), menu flows, LOVs, messages.
Stored procedure call sites and dependencies.
Create Rule Catalog:
Decision table format:
RuleID | Condition | Action | Source | TestCaseID R-001 | loanAmt > 1,000,000 & score < 750 | ManualReview | LOAN_FORM.trigger | TC-001
Generate a traceability matrix: UI element ↔ trigger ↔ PL/SQL ↔ requirement.
Phase 2 — Design & Contracts
Define bounded contexts (KYC, Customer, Credit, Loan, Pricing, Disbursement).
Write OpenAPI contracts for each service (consumer-driven contracts). Example snippet:
POST /eligibility/assess:
request:
customerId: string
productCode: string
amount: decimal(18,2)
tenure: int
scoreSnapshot: { score:int, fetchedAt:datetime }
response:
decision: APPROVE | REJECT | MANUAL_REVIEW
reasons: [ { code, message } ]
limits: { maxAmount, maxTenure }
Design events (Kafka topics): kyc.completed, credit.score, loan.decided, payment.disbursed. Use Avro/Protobuf + Schema Registry.
Phase 3 — Pilot Implementation (KYC example)
Angular UI: Reactive forms, client validators for format, async validators that call CustomerService/validatePan.
RuleService:
Implement extracted rules using a domain model + unit tests.
Use BigDecimal consistently for money: money scale = 2, interest scale = 4, rounding = RoundingMode.HALF_UP.
Java snippet (validation):
BigDecimal amount = request.getAmount().setScale(2, RoundingMode.HALF_UP);
if (amount.compareTo(new BigDecimal("10000.00")) < 0) {
throw new ValidationException("E_MIN_AMT","Minimum amount is 10,000.00");
}
Cache: Redis for reference data (pan existence, branch lists). Use Spring @Cacheable/@CachePut/@CacheEvict patterns.
Phase 4 — Event & Read models
Implement Kafka streams/local processors to update materialized read models (ElasticSearch/Cosmos) for UI reads (CQRS).
BFF or API aggregates from read models to give fast UI responses (<50ms ideal).
Phase 5 — Data migration & cutover
Build ETL/CDC pipelines (Debezium → Kafka → Data Lake) for data migration and reporting.
Set dual-write or change capture for short overlap phase; reconcile using golden dataset.
7 — Key technical patterns & why
CQRS + Materialized Read Models
Avoids expensive joins/transactions for UI; read models updated via streams.
Cache-aside (Redis)
Hot lookups (KYC status, last credit score, config) stored in Redis to minimize DB hits and achieve <10ms read times. Use TTL and event-driven invalidation.
Event-Driven (Kafka)
Decouples microservices; enables async enrichment (bureau fetches), replayability for analytics and RAG.
Strangler Fig
Replace one feature at a time; temporary adapters wrap PL/SQL; business continued on legacy while new features launch.
Domain-Driven Design
Bounded contexts map to microservices; SMEs validate aggregates and boundaries.
8 — Data residency & multi-region strategy
Per-region data sovereignty: all PII and transaction records reside only in the user’s home region (India, US, UK).
Global reporting: use aggregate-only replication (no raw PII). Regions publish sanitized aggregates to a global analytics store.
Failover policy:
Use Traffic Manager with nested profiles: top-level Geo routing to child profiles, child profiles using Priority to failover to other region only for service availability (for compliance-critical flows, failover may be limited to same-jurisdiction DR).
Emergency governance: documented approvals for any cross-border key releases (if encrypted replica approach used).
9 — RAG layer & Agentic AI integration
Retrieval store: Vector DB for embeddings (policies, product docs, decision rationale) + structured feature store (150+ features aggregated per decision).
RAG flow:
Query builds context (customer features, decision history, regulatory constraints) → retrieval returns docs + embeddings → LLM synthesizes recommended action → agentic orchestrator applies rules and calls services.
Agentic AI:
Orchestrator ingests events (loan application) → fetches data → runs models (fraud, risk) → suggests or executes decision (auto approve/refer).
Persist decision rationale for auditability and XAI.
Security: No raw PII passes to non-authorized LLM endpoints; PII must be tokenized/anonymized before RAG.
10 — Security, governance & compliance
Identity: Azure AD SSO, OAuth2/OpenID Connect; RBAC for services.
Secrets: Azure Key Vault for keys & certificates; HSM for cryptographic keys.
Network: Hub-spoke VNets, private endpoints, Azure Firewall + NSGs, no public endpoints for DB/Redis/Kafka.
Logging & SIEM: Application Insights + Log Analytics + Sentinel. Audit trails for all decisions.
Policy as Code: Azure Policy to forbid public IPs, require diagnostic settings.
MFA & fraud controls: Replace SMS OTP for critical flows with push/TOTP/FIDO2, add risk-based step-ups.
11 — DevSecOps & Automation
IaC: Terraform/Bicep modules for landing zone, VNet, AKS, SQLMI, Redis, Kafka (or Confluent Cloud + Private Link).
CI/CD: Pipelines (Azure DevOps / GitHub Actions) for build/test/deploy; separate pipelines for infra and apps.
Quality Gates: SonarQube, Snyk, container scanning (Trivy), IaC scanning (Checkov).
Automated tests: Unit, contract (Pact), integration, regression (Selenium/Cypress), performance (Gatling/JMeter).
Policy enforcement: Gate pipeline on Azure Policy and security checks.
12 — Testing & validation plan
Golden dataset replay: run historical transactions against new system and compare output with legacy.
Contract tests: ensure API behavior; consumer-driven contract approach.
Parallel run: select pilot users and run both systems; reconcile differences.
Performance testing: API P95/P99; Redis hit ratio; Kafka consumer lag; DB p99 < target.
Chaos & DR drills: simulate region outage and validate failover.
13 — KPIs to track (program + run time)
Program KPIs
% Forms migrated (target incremental).
% PL/SQL logic refactored/wrapped.
IaC automation coverage (%) — target > 90% by Phase 4.
CI/CD deployment frequency, change failure rate.
Technical / Ops KPIs
API P95 latency < 200ms; P99 < 500ms.
Redis hit ratio > 80%.
Kafka consumer lag < threshold (seconds).
MTTR < 30 minutes; MTTD < 5 minutes.
Business KPIs
Loan automated decision rate (%).
Onboarding TAT (minutes).
Customer satisfaction (CSAT).
Cost reduction % (infra and ops).
14 — Risks & mitigations (top items)
Hidden business rules in PL/SQL — Mitigation: exhaustive extraction and SME workshops; golden test dataset.
Data residency violation — Mitigation: per-region data store, policy-as-code, legal sign-off.
Performance regressions — Mitigation: optimize APIs, caching, read models; performance testing early.
User adoption — Mitigation: UX workshops, keyboard shortcuts, training, phased rollout.
Operational skill gap — Mitigation: CoE, training, hire core cloud engineers.
15 — Runbooks (cutover + emergency failover) — short templates
Cutover runbook (module)
Freeze config changes on legacy form.
Enable dual-write (if applicable).
Deploy microservice and Angular module behind feature flag.
Switch traffic for pilot user group.
Monitor reconciliation for 24–72 hours.
If mismatch > threshold, rollback to legacy route and investigate.
After stable, remove adapter and decommission legacy form.
Emergency cross-border key release (governance)
Request initiated by Ops with business justification.
Privacy officer & Legal approval recorded.
Time-bounded key release via automation (Key Vault release API) with TTL.
Audit logs collected and archived; post-incident review required.
16 — Sample artifacts & snippets
Decision table example (loan eligibility)
RuleID | Condition | Action |
R-001 | loanAmount > 1,000,000 & creditScore < 750 | Manual Review |
R-002 | creditScore >= 750 & DTI < 0.5 | Auto Approve |
R-003 | previousDefaults > 0 | Reject |
Angular reactive validator (PAN example)
panControl = this.fb.control('', [
Validators.required,
Validators.pattern('[A-Z]{5}[0-9]{4}[A-Z]{1}')
]);
// async validator
panControl.setAsyncValidators(this.panExistsValidator.bind(this));
Spring BigDecimal & rounding
public static final int MONEY_SCALE = 2;
public static final RoundingMode MONEY_ROUNDING = RoundingMode.HALF_UP;
BigDecimal amount = new BigDecimal("1234.5678").setScale(MONEY_SCALE, MONEY_ROUNDING);
Spring cache example
@Cacheable(value="kycCache", key="#customerId")
public KycDetails getKyc(String customerId) {...}
@CacheEvict(value="kycCache", key="#customerId")
public void evictKyc(String customerId) {...}
Kafka topic design
kyc.requested (regionA)
kyc.completed (regionA)
loan.decided (regionA)
Mirror only sanitized analytics.loan.aggregates to global analytics cluster.
17 — Roles & team composition
Program Sponsor & PMO — business steering.
Enterprise Architect — overall design & governance.
Solution Architects (Cloud, Data, Security).
SMEs (Business) — forms & rules validation.
Dev Teams — frontend, backend, platform (DevOps), data engineers.
Test/QA — automation, performance.
Security & Compliance — approvals, audits.
18 — Example timeline (condensed)
Prep & Discovery: 1 month
Pilot (KYC): 1.5 months
Main migration (loan origination + credit): 3–4 months
Multi-region + reporting + decommission: 1–2 monthsTotal: ~6–8 months for core product (varies by scale).
19 — Deliverables to present to stakeholders
Executive one-pager (value & ROI)
Target architecture diagram (text + visual)
Migration roadmap with phases & timeline
Business Rule Catalog & Decision Tables
Pilot success report (metrics & reconciliation)
Go-live Checklist & Runbooks
KPI dashboard (Power BI) with program & operational metrics
20 — Final recommended next steps (actionable)
Run quick 2–4 week discovery: Forms2XML + rule catalog + golden dataset creation.
Build Azure landing zone + IaC skeleton and CI/CD pipeline.
Implement KYC pilot and validate with SMEs and golden dataset.
Iterate: migrate top 3 high-value modules next and add event streaming.
Enforce data residency and policy-as-code from day-1.
Key Challenges & How We Solved Them
Challenge | Solution |
Hidden business rules deeply nested inside PL/SQL | Used parsing tools + manual code reviews to extract and catalog them |
Intermixed data access + UI logic | Separated concerns: moved data logic to repository layer, UI logic to Angular components |
Risk of missing edge cases | Built regression test suite with production data snapshots |
Resistance from business users (fear of rule change) | Conducted workshops with side-by-side rule comparison before deployment |
Case study — Extract & migrate PL/SQL business rules (Digital Lending)
Context
Legacy: Oracle Forms + PL/SQL packages implement KYC, eligibility, and loan rules.Goal: Extract rules, validate with SMEs, and implement them as reusable business rules served by RuleService (Drools/DMN or Spring Boot) + client UX validators in Angular.
Step 1 — Parse PL/SQL & detect candidate rules
Process
Collect PL/SQL sources from DBA_SOURCE, .pkb/.pks and Forms2XML outputs.
Static parse using a PL/SQL parser (ANTLR PL/SQL grammar or an off-the-shelf parser) to produce ASTs.
Pattern match AST for IF/ELSIF/CASE, RAISE_APPLICATION_ERROR, DBMS_OUTPUT, comparisons, SELECT that feed conditions.
Extract conditions, variables, constants, messages and store as structured rule candidates (JSON).
SME workshop to validate, de-duplicate, and prioritize.
Example PL/SQL (legacy)
-- pkg_loan_validation.pkb
PROCEDURE validate_application(p_app_id IN NUMBER) IS
v_score NUMBER;
v_amount NUMBER;
BEGIN
SELECT credit_score, requested_amount INTO v_score, v_amount
FROM loan_applications WHERE app_id = p_app_id;
IF v_score < 650 AND v_amount > 500000 THEN
RAISE_APPLICATION_ERROR(-20001, 'Manual review required: low score & high amount');
END IF;
IF v_amount > credit_limit_for_customer(p_app_id) THEN
RAISE_APPLICATION_ERROR(-20002, 'Loan exceeds credit limit');
END IF;
IF get_customer_age(p_app_id) < 18 THEN
RAISE_APPLICATION_ERROR(-20003, 'Customer must be 18+');
END IF;
END validate_application;
Extracted rule candidates (structured)
[
{
"ruleId": "R-LOAN-001",
"condition": "credit_score < 650 AND requested_amount > 500000",
"action": "MANUAL_REVIEW",
"source": "pkg_loan_validation.pkb:validate_application",
"message": "Manual review required: low score & high amount"
},
{
"ruleId": "R-LOAN-002",
"condition": "requested_amount > credit_limit",
"action": "REJECT_OR_MANUAL",
"source": "pkg_loan_validation.pkb:validate_application",
"message": "Loan exceeds credit limit"
},
{
"ruleId": "R-CUST-001",
"condition": "customer_age < 18",
"action": "REJECT",
"source": "pkg_loan_validation.pkb:validate_application",
"message": "Customer must be 18+"
}
]
Step 2 — Create Decision Table (DMN / spreadsheet) & SME sign-off
RuleID | Condition | Action | Priority | Notes |
R-LOAN-001 | credit_score < 650 AND requested_amount > 500000 | MANUAL_REVIEW | 100 | keep as referral |
R-LOAN-002 | requested_amount > credit_limit | REJECT | 200 | check credit_limit function |
R-CUST-001 | customer_age < 18 | REJECT | 10 | age calc uses DOB |
SMEs review and confirm thresholds and message text. This decision table is stored in the Rule Catalog.
Step 3 — Implementation options & decision
Option A (Rule engine): Use Drools / DMN for dynamic rule management (business team can update non-programmatic rules).
Option B (Code): Implement in Spring Boot service code for performance/traceability-critical rules.
We implemented both:
Core deterministic constraints (age, credit limit) → service code (fast, auditable).
Business policy thresholds and promotions → Drools/DMN so product owners can tune.
Step 4 — Example conversions
A. Drools Rule (R-LOAN-001)
package com.bank.rules;
import com.bank.domain.LoanApplication;
rule "R-LOAN-001 Manual review for low score & high amount"
when
$app : LoanApplication( creditScore < 650, requestedAmount > 500000 )
then
$app.addDecision(new Decision("MANUAL_REVIEW", "Manual review required: low score & high amount"));
// stop or continue depending on flow
end
B. Java service check (R-CUST-001 & R-LOAN-002)
@Service
public class LoanValidationService {
public void validateLoan(LoanApplicationDto app) {
// BigDecimal arithmetic with fixed scale
BigDecimal amount = app.getRequestedAmount().setScale(2, RoundingMode.HALF_UP);
BigDecimal creditLimit = creditLimitService.getLimitForCustomer(app.getCustomerId());
if (app.getCustomerAge() < 18) {
throw new BusinessValidationException("R-CUST-001", "Customer must be 18+");
}
if (amount.compareTo(creditLimit) > 0) {
throw new BusinessValidationException("R-LOAN-002", "Loan exceeds credit limit");
}
// for rules engine conditions
Map<String, Object> facts = Map.of("creditScore", app.getCreditScore(), "requestedAmount", amount);
List<Decision> decisions = droolsService.evaluate("loanRules", facts);
// act on decisions...
}
}
Step 5 — Frontend validation (Angular)
We implement fast UX validation in Angular reactive forms for immediate feedback and call server async validations for uniqueness / cross-field checks.
Angular reactive validator (age check + async PAN uniqueness)
// simple sync validator for age
function minAgeValidator(minAge: number): ValidatorFn {
return (control: AbstractControl) => {
const dob = new Date(control.value);
const age = calculateAge(dob);
return age < minAge ? { minAge: { required: minAge } } : null;
};
}
// async validator for PAN uniqueness calling backend API
panControl.setAsyncValidators(this.panExistsValidator.bind(this));
panExistsValidator(control: AbstractControl): Observable<ValidationErrors | null> {
return this.http.post('/api/customers/check-pan', { pan: control.value })
.pipe(map((res:any) => res.exists ? { panExists: true } : null));
}
Step 6 — Unit & Integration tests
Unit test (Java, JUnit)
@Test
void whenCustomerUnder18_thenThrowException() {
LoanApplicationDto app = createLoanAppWithAge(17);
Assertions.assertThrows(BusinessValidationException.class, () -> loanValidationService.validateLoan(app));
}
@Test
void whenCreditLimitExceeded_thenThrowException() {
LoanApplicationDto app = createLoanAppWithAmount(new BigDecimal("1000000")); // 1M
when(creditLimitService.getLimitForCustomer(any())).thenReturn(new BigDecimal("500000"));
BusinessValidationException ex = assertThrows(BusinessValidationException.class, () -> loanValidationService.validateLoan(app));
assertEquals("R-LOAN-002", ex.getCode());
}
Contract / Golden dataset test
Replay historical loan applications through legacy PL/SQL and new service; compare decisions & error messages.
Any mismatch is flagged and reviewed with SME; update rule mapping or fix logic.
Step 7 — Traceability & governance artifacts
Traceability matrix (sample)
Legacy Object | Extracted Rule ID | Decision Table Row | New Implementation |
pkg_loan_validation.validate_application | R-LOAN-001 | Row 1 | Drools rule "R-LOAN-001" + Flag in LoanService |
pkg_loan_validation.validate_application | R-LOAN-002 | Row 2 | LoanValidationService.checkCreditLimit() |
Rule Catalog entry (metadata)
ruleId: R-LOAN-001
description: "Manual review required for low score & high amount"
source: pkg_loan_validation.pkb:validate_application
implementation: DROOLS | service
priority: 100
owner: Credit Risk Team
testCases: [TC-LOAN-001]
audit: enabled
All rule changes are versioned (Git) and exposed in an admin UI for rule owners (if managed in DMN/Drools).
Step 8 — Edge cases & challenges we handled
Hidden dependencies: PL/SQL used helper functions (e.g., credit_limit_for_customer). We extracted and refactored such helpers into services (CreditLimitService) and wrote integration tests.
Complex SQL conditions: When conditions used complex subqueries, we converted them to precomputed fields (materialized views or ETL job) so rules evaluate on simple facts.
Rounding & precision: All monetary arithmetic uses BigDecimal with constants for scale & RoundingMode.HALF_UP. We kept high internal precision during intermediate calc and round only at persistence/display boundaries.
Performance: Heavy rule sets were evaluated via Drools session initialization caching and facts prepopulation; critical checks kept in plain Java (fast path).
SME sign-off: every rule conversion had an automated diff (legacy decision vs new decision) and SME acceptance before going live.
Practical tooling & pipeline (recommended)
Extraction: Forms2XML + DB extraction (DBA_SOURCE) + ANTLR or open-source PL/SQL parser.
Rule Catalog: CSV/Excel → import to DMN registry or rule admin UI.
Rule Engine: Drools / Camunda DMN for business-editable rules; Spring Boot wrapper for service access.
CI/CD: Pipeline runs unit tests, contract tests, golden dataset replay, and deploys rule changes to staging for SME testing.
Observability: Log decisions with correlationId and store decision snapshots for audit/compliance and explainability.
Interview / Presentation soundbite
“We parsed PL/SQL by extracting sources and using AST parsing + pattern matching to surface IF/CASE conditions and RAISE_APPLICATION_ERROR calls as rule candidates. SMEs validated decision tables. Deterministic checks were implemented in Spring Boot with BigDecimal precision; policy rules were migrated to Drools/DMN for business manageability. Each rule had automated parity tests against a golden dataset and traceability back to the original PL/SQL object.”
Comments