Skip to content

5.2 Services and modules

For an early-stage NBFC building this platform, the correct shape on day one is a modular monolith — a single deployable with internally-enforced module boundaries — with selective extraction of services for specific scale, regulatory, or partner-integration reasons.

A pure microservices architecture on day one is a mistake. See the why-not-microservices discussion in 5.1 for the full rationale.

Two deployables at MVP:

  1. core-platform — modular monolith holding most modules; serves all synchronous request flows.
  2. async-workers — same codebase deployed for async / scheduled work (NACH file generation, daily classification, accruals, settlements, ETL, periodic refresh).

Optionally a third:

  1. partner-gateway — partner-facing APIs deployed with separate network isolation and stricter security perimeter. Justified once the first co-lending partner integration is live.

Inside core-platform, module boundaries are enforced via package structure and CI checks, not via separate services. This keeps the boundaries adjustable without rewriting deployment infrastructure.

core-platform/
├── modules/ # Domain modules — one per product family
│ ├── acquisition/ # Section 3.A
│ ├── application/ # Section 3.B
│ ├── kyc/ # Section 3.C
│ ├── ingestion/ # Section 3.D
│ ├── decisioning/ # Section 3.E (engine)
│ ├── manualreview/ # Section 3.F
│ ├── colending/ # Section 3.G
│ ├── documentation/ # Section 3.H
│ ├── disbursement/ # Section 3.I
│ ├── lms/ # Section 3.J
│ ├── collections/ # Section 3.K
│ ├── monitoring/ # Section 3.L
│ ├── accounting/ # Section 3.M
│ ├── reporting/ # Section 3.N
│ ├── admin/ # Section 3.O
│ ├── analytics/ # Section 3.P (operational view)
│ └── notification/ # Cross-cutting
├── integrations/ # Vendor adapter implementations
│ ├── bureau-cibil/, bureau-experian/, ...
│ ├── kyc-karza/, kyc-idfy/, ...
│ ├── aa-setu/, aa-finbox/, ...
│ ├── bsa-perfios/, ...
│ ├── gst-cygnet/, ...
│ ├── esign-leegality/, esign-digio/, ...
│ ├── nach-digio/, ...
│ ├── payment-razorpay/, payout-razorpayx/, ...
│ ├── sms-gupshup/, whatsapp-gupshup/, email-ses/, ivr-exotel/, ...
│ └── partner-bank-{name}/
├── shared/ # Cross-module concerns
│ ├── audit/
│ ├── consent/
│ ├── pii/
│ ├── workflow/
│ ├── rules/
│ ├── tracing/
│ └── exception/
└── api/ # REST controllers; per-channel BFFs

For the concrete package skeleton, see 5.12 Development conventions.

A module’s public API is what other modules can call. Internals are off-limits.

// Allowed: cross-module call via public API
class DisbursementService {
private final SanctionService sanctionService; // public API
private final MandateService mandateService; // public API
void execute(Long sanctionId) {
Sanction s = sanctionService.getById(sanctionId);
Mandate m = mandateService.getActiveFor(s.getApplicationId());
// ...
}
}
// NOT allowed: direct DB access into another module
class DisbursementService {
private final SanctionRepository sanctionRepo; // BANNED — internal to sanction module
}

Enforce with ArchUnit tests in CI:

@ArchTest
public static final ArchRule modules_only_depend_via_api =
classes()
.that().resideInAPackage("..modules.disbursement..")
.should().onlyDependOnClassesThat(
resideInAnyPackage(
"..modules.disbursement..",
"..modules.{other}.api..", // each module exposes a stable api/ package
"..shared..",
"..integrations..",
"java..", "org.springframework.."
)
);

Database access: each module owns its own logical schema; cross-module access via service methods only. This keeps schema refactors local and prevents quietly-acquired coupling.

Promote a module out of the monolith when one or more of these is true:

  1. Independent scaling profile — the module needs different scaling characteristics than the rest. Example: LMS daily-batch jobs need 10× memory at night.

  2. Different security perimeter — the module faces a different external attack surface. Example: partner gateway needs mTLS and stricter rate limiting that doesn’t apply to internal flows.

  3. Different team ownership that’s diverging. Example: a dedicated co-lending team that wants independent release cadence.

  4. Build / deploy pace conflict — the module’s tests slow the whole monolith’s deploy. Example: heavy reporting integration tests.

  5. Compliance isolation — regulator-driven need for explicit data isolation. Rare but real for some white-label / SaaS productisation scenarios.

Recommended extraction order across phases:

  1. Partner gateway (Phase 2) — partner mTLS + network isolation justify it early.
  2. Documentation / DMS (Phase 3) — object-storage-heavy; different scaling.
  3. LMS daily batch (Phase 4) — performance isolation.
  4. Co-lending allocation + settlement (Phase 4) — when multi-partner adds operational complexity.
  5. Analytics platform (independent from day 1) — separate data plane via warehouse.

Several cross-module concerns are library-shaped, not service-shaped:

  • Consent SDK — every module uses; consent recording is a library API.
  • Audit SDK — wrappers that emit standardised audit events with hash-chain integrity.
  • PII tokenisation — wraps reads/writes; clear text only where allowed.
  • Workflow SDK — wraps the workflow engine (Camunda or Temporal) for cleaner usage.
  • Rule engine SDK — wraps Drools / decision tables for rule loading and execution.
  • Vendor adaptor base — every vendor adapter implements a common interface with retry, circuit-breaker, observability, audit logging.

These ship as Maven modules within the monorepo. Versioned but rarely-changing.

  • External (channel UIs, partners): REST + JSON; OAuth2 (borrowers) or mTLS (partners); versioned in URL.
  • Internal (service-to-service inside monolith): direct Java method calls via public API; no network hop.
  • Internal (service-to-service across deployment): REST + JSON or gRPC; mTLS; short-lived JWT for service identity.
  • Async (cross-module event): Kafka / RabbitMQ topics with schema-registry-versioned events.

The architecture is shaped by team strength, not the other way around. Stages:

PhaseTeamShape
MVP (phase 1)One platform team of 5 – 8 engineersSingle team owns monolith; module ownership rotates
Scale (phase 2 – 3)15 – 22 engineersOrigination team, LMS team, collections team, co-lending team, platform/infra team
Productisation (phase 5+)35 – 55+ engineersPer-module teams + dedicated platform team + DevOps / SRE

See 9.2 Headcount by AUM for team sizing.

Modular monolith vs microservices revisited

Section titled “Modular monolith vs microservices revisited”

The modular monolith is not a stepping stone to microservices automatically. Many platforms operate happily as modular monoliths at > ₹500 Cr AUM. The trigger to extract isn’t “we’ve gotten big enough” — it’s the specific operational or business need listed above.

Keep modules clean; defer service extraction to when it genuinely helps. Premature microservices is one of the most expensive engineering mistakes a fintech can make.