Keep deployment and modularity separate
A modular monolith organizes an application into cohesive modules with explicit interfaces while retaining a shared deployment boundary. It can reduce operational overhead compared with distributing every capability, but it still needs deliberate dependency management.
One database is a common choice, not a defining requirement. The important question is which module owns a piece of data and how other modules are allowed to use it.
Name boundaries after capabilities
For a hypothetical order application, consider catalog, ordering and billing modules. Each exposes a small interface. Ordering can ask catalog for product information without importing its internal repository or writing its tables directly.
A useful structure might be:
modules/
catalog/
public/
internal/
ordering/
public/
internal/
billing/
public/
internal/
Those directory names are only a convention until checks enforce them. TypeScript path aliases help resolve imports; they do not prohibit importing an internal file. TypeScript paths reference.
Enforce the dependency graph
Add a rule that disallows references to another module's internals and catches cycles between modules. In a Java application, Spring Modulith provides module-structure verification, including dependency-cycle and API-access checks. Its exact configuration should match the chosen release. Spring Modulith verification.
For a TypeScript application, use an appropriate dependency-analysis or lint rule and verify it with an intentionally invalid import. A rule that only documents the convention but never fails the build is easy to bypass accidentally.
Keep shared code narrow. A shared directory containing every entity and service quickly recreates the coupling the modules were meant to prevent.
Transactions need an owner
A shared database can make some local atomic operations easier. It does not make an external payment, email or object-storage write part of that same transaction.
Define the transaction boundary for each business operation. If one module must react after another commits, decide whether an in-process call, durable event publication or another mechanism fits the failure requirements.
Avoid holding a database transaction open while waiting for an unpredictable external service. Decide how an uncertain external outcome is reconciled.
Understand the tradeoffs
A shared deployment can simplify releases and local setup, while coupling the modules' release artifact and some failure modes. Independently deployed services can provide different scaling or runtime boundaries, but introduce network contracts and operational work.
Neither choice follows from a fixed engineer count. Evaluate ownership, workload characteristics, deployment frequency and the team's ability to operate the system.
Leave extraction as an option
A clear interface can make future extraction easier, but moving a module across a network still changes latency, failures and consistency. Do not promise extraction as a mechanical packaging step.
Before extracting billing, for example, inventory its direct table access, synchronous callers and transaction assumptions. Replace hidden dependencies first. Test the public contract within the monolith, then add the failure behavior required by a remote boundary.
A successful modular design lets a reviewer understand what can change together and which rules protect that understanding.

Sardar Mukhshaf
Full-Stack & DevOps Engineer
I build scalable web applications, secure backend systems, and production-ready cloud infrastructure. My engineering focus spans interface design, API architecture, deployment pipelines, and observability. Follow for more deep dives into system architecture, performance engineering, and emerging technologies.
More from the practice
Keep the useful ideas close.
Follow new articles with your preferred RSS reader.
Open the RSS feed ↗


