5.2 Services and modules
The decision: modular monolith first
Section titled “The decision: modular monolith first”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.
Initial deployment topology
Section titled “Initial deployment topology”Two deployables at MVP:
core-platform— modular monolith holding most modules; serves all synchronous request flows.async-workers— same codebase deployed for async / scheduled work (NACH file generation, daily classification, accruals, settlements, ETL, periodic refresh).
Optionally a third:
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.
Module layout
Section titled “Module layout”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 BFFsFor the concrete package skeleton, see 5.12 Development conventions.
Module boundary enforcement
Section titled “Module boundary enforcement”A module’s public API is what other modules can call. Internals are off-limits.
// Allowed: cross-module call via public APIclass 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 moduleclass DisbursementService { private final SanctionRepository sanctionRepo; // BANNED — internal to sanction module}Enforce with ArchUnit tests in CI:
@ArchTestpublic 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.
When to extract a module to a service
Section titled “When to extract a module to a service”Promote a module out of the monolith when one or more of these is true:
-
Independent scaling profile — the module needs different scaling characteristics than the rest. Example: LMS daily-batch jobs need 10× memory at night.
-
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.
-
Different team ownership that’s diverging. Example: a dedicated co-lending team that wants independent release cadence.
-
Build / deploy pace conflict — the module’s tests slow the whole monolith’s deploy. Example: heavy reporting integration tests.
-
Compliance isolation — regulator-driven need for explicit data isolation. Rare but real for some white-label / SaaS productisation scenarios.
Recommended extraction order across phases:
- Partner gateway (Phase 2) — partner mTLS + network isolation justify it early.
- Documentation / DMS (Phase 3) — object-storage-heavy; different scaling.
- LMS daily batch (Phase 4) — performance isolation.
- Co-lending allocation + settlement (Phase 4) — when multi-partner adds operational complexity.
- Analytics platform (independent from day 1) — separate data plane via warehouse.
Shared libraries (not services)
Section titled “Shared libraries (not services)”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.
API patterns by edge
Section titled “API patterns by edge”- 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.
Team shape
Section titled “Team shape”The architecture is shaped by team strength, not the other way around. Stages:
| Phase | Team | Shape |
|---|---|---|
| MVP (phase 1) | One platform team of 5 – 8 engineers | Single team owns monolith; module ownership rotates |
| Scale (phase 2 – 3) | 15 – 22 engineers | Origination team, LMS team, collections team, co-lending team, platform/infra team |
| Productisation (phase 5+) | 35 – 55+ engineers | Per-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.
Related
Section titled “Related”- 5.12 Development conventions — concrete repo skeleton, Spring Boot patterns, testing.
- 5.4 Events and async — when modules speak via events.
- 5.9 Schema reference — DDL with one schema per module.
- 9.2 Headcount by AUM — team scaling.