Skip to content
Svayam Labs

Blueprint 01 · Structural

The Modular Monolith with a Pure Domain Core

One thing to deploy. Rigid layering inside. The business rules that matter most live as pure, side-effect-free functions — testable in isolation, swappable in place, and ready to be carved into services the day scale demands it.

Style · Modular monolith Layering · controller → domain → repository Core · pure functions / FSM Exit · split-to-services path
Context & intent

A monolith you won't regret

Microservices are an organisational tool that is frequently paid for in distributed-systems tax — network failures, eventual consistency, deployment choreography — long before a team is big enough to need them. This blueprint takes the opposite bet: keep one deployable, but build it as if it were already several.

The trick is internal discipline. Each feature is a self-contained module with the same three-layer shape, every module talks to the database through a single repository boundary, and the genuinely important logic is pulled out into a pure core that knows nothing about HTTP, the ORM, or the framework. The result feels like a small system to run and a large one to reason about.

Why this shape

The expensive part of a service split is not the network plumbing — it's untangling business logic that has leaked into controllers, jobs, and database triggers. If the logic is already isolated and pure, the split becomes mechanical: lift a module, give it its own process, and replace an in-process call with a network call. The hard work was done up front, cheaply.

The core idea

Three layers, one pure core

Thin transport layer

Controllers / route handlers do only transport work: validate input, resolve identity and tenant from the session, enforce roles, then delegate. No business logic lives here.

Pure domain core

Scoring engines, workflow state machines, and rule evaluators are pure functions: feed them a context object, get a decision back. No database, no clock, no network — so they are trivially unit-testable and fully deterministic.

Single repository boundary

Every database access goes through one repository / storage abstraction per module. Domain code persists only through it, so the ORM is mockable in tests and swappable in production.

One contract, defined once

A recurring multiplier in these systems is a single source of truth for the data contract. The database schema is declared once; validation schemas and language-level types are derived from it; and both the server and the client import the same definitions. There is no second place for the shape of an entity to drift. When a column changes, the type error shows up everywhere that matters at compile time, not in production.

Variation seen in practice: the workflow as a state machine

When the domain is a multi-step process with gated transitions — intake, review, approval, closure — the pure core is best expressed as a finite state machine. The application layer hydrates a transition context from the database; the FSM is a pure function that returns either a valid next state or a rejection. Only valid transitions are ever persisted, and the riskiest logic in the system has zero I/O to mock.

Reference architecture

How the pieces sit together

FIG 1.1 · Single deployable, layered internals, derived contract
flowchart TB
    subgraph client["Client tier"]
      SPA["Single-page app / installable client"]
    end
    subgraph deploy["Single deployable process"]
      direction TB
      RT["Transport layer
routes · validation · authZ"] subgraph mods["Feature modules"] direction LR M1["Module A"] M2["Module B"] M3["Module C"] end CORE["Pure domain core
engines · FSM · rules
(no I/O)"] REPO["Repository boundary
(single SQL surface)"] end SHARED["Shared contract
schema → types → validators"] DB[("Relational database")] SPA -->|HTTP / JSON| RT RT --> mods mods -->|"call with context"| CORE CORE -->|"decision / result"| mods mods --> REPO REPO --> DB SHARED -.imported by.-> SPA SHARED -.imported by.-> RT SHARED -.shapes.-> REPO

Component responsibilities

ComponentOwnsMust never
Transport layerInput validation, session/identity/tenant resolution, role checks, response shapingContain business rules or touch the ORM directly
Feature moduleOrchestrating a use case: load state, call the core, persist results, emit eventsReach into another module's tables
Pure domain coreThe decision itself — scoring, transitions, rule evaluationPerform I/O, read the clock, or import the framework
Repository boundaryEvery read and write for its module; the only place SQL/ORM livesEncode business logic or cross module lines
Shared contractCanonical schema, derived types and validators used by all tiersHave a second, hand-maintained copy anywhere
A request, end to end

The path of a single operation

FIG 1.2 · Decision computed by the pure core, persisted through the repository
sequenceDiagram
    participant C as Client
    participant T as Transport layer
    participant M as Feature module
    participant K as Pure core
    participant R as Repository
    participant D as Database
    C->>T: Request + session cookie / token
    T->>T: Validate input · resolve tenant & role
    T->>M: Delegate use case
    M->>R: Load current state
    R->>D: SELECT
    D-->>M: State
    M->>K: evaluate(context)
    K-->>M: Decision (pure, deterministic)
    M->>R: Persist result + audit entry
    R->>D: INSERT / UPDATE (one transaction)
    M-->>T: Result
    T-->>C: JSON response
        
Compute-on-write, not a job queue

Derived values — deltas against a baseline, threshold alerts, reconciled scores — are calculated inline at write time by the pure core rather than by a separate background worker. Until throughput genuinely demands asynchronous processing, this keeps the system to a single moving part and makes every result reproducible from its inputs.

Where else this design fits

Five alternate use cases

Fintech

Loan origination engine

Eligibility, risk scoring and offer generation are pure functions over an applicant context; the monolith orchestrates KYC, underwriting and disbursal as gated FSM stages — auditable and easy to certify.

Logistics

Order & fulfilment workflow

An order moves placed → picked → packed → shipped → delivered as a state machine. The pure core validates every transition; the repository keeps each warehouse's data behind one boundary.

EdTech

Assessment & grading platform

Rubric scoring and progression rules live in a deterministic core that can be replayed and unit-tested against fixtures, while the surrounding monolith handles enrolment, submissions and reporting.

GovTech

Permit & licence issuance

Application review with mandatory approval steps maps cleanly to a gated FSM; the single-deployable footprint suits constrained on-premise or state-data-centre hosting.

B2B SaaS

Subscription & entitlement service

Plan changes, proration and feature gating are pure calculations over an account context; one deployable keeps early operating cost low while the boundaries pre-stage a later billing-service split.

Trade-offs & risks

What this costs you

RiskWhy it happensMitigation
Boundary erosionUnder deadline pressure a module reaches into another's tables, quietly recreating a distributed ball of mud inside one processEnforce module isolation with import/path-alias rules in CI; one repository per module; code review on cross-module access
Single scaling unitA hot path forces you to scale the whole process, paying for idle capacity in cold modulesScale horizontally behind a load balancer first; the pre-drawn seams let you peel off only the hot module when that stops being enough
Deploy blast radiusOne bad change can take down every feature at onceStrong typing from the shared contract, comprehensive pure-core tests, and progressive rollout / health checks
"Flexible column" sprawlSchema-light JSON sub-documents avoid migrations but can drift into undocumented, unvalidated shapesValidate JSON sub-documents against derived schemas; promote stable fields into real columns over time
Security notes

Getting trust right inside one process

Identity from the session, authorisation per handler

Because everything shares a process, it is tempting to trust data passed between layers. Don't. Resolve the acting user, role and tenant from the verified session at the transport edge and pass them down explicitly; never accept them from the request body. Apply ownership and tenant checks in the handler, not just at login.

  • Hash credentials with a slow algorithm; strip secrets from every response shape at the boundary.
  • Make auditing a cross-cutting concern: write an append-only entry — ideally with before/after snapshots — in the same transaction as the mutation, so the trail can never disagree with the data.
  • Beware the convenient defaults a monolith accumulates: fallback session secrets, insecure cookie flags, seeded default passwords, and destructive "reset" endpoints guarded only by a confirmation string. Gate all of these behind real environment configuration before production.
Decision guide

When to reach for it — and when not to

Use it when

  • A small team needs to ship a feature-rich product fast without operating a fleet of services.
  • The domain has clear, valuable business rules worth isolating and testing exhaustively.
  • You want the option to split into services later without betting the architecture on it now.
  • Operational simplicity and low cost matter — constrained hosting, early-stage budgets.

Avoid it when

  • Different parts genuinely need independent scaling, languages, or release cadences today.
  • Multiple autonomous teams will contend over one codebase and one deploy pipeline.
  • Components have wildly different availability or compliance isolation requirements.
  • The team lacks the discipline to keep module boundaries honest — without it, this degrades fast.
Observed across several systems in the portfolio: full-stack records platforms, a clinical-operations backend built on clean/hexagonal layering, and a compliance API — each independently arrived at a thin-controller / pure-core / repository split with a single derived data contract.

Want this pattern applied to your build?

These are the structures we reach for. Tell us the system you're trying to build.