Architecture

Choose a modular monolith before reaching for microservices

Kamruz
Kamruz
Oct 14, 2026
5 min read
Choose a modular monolith before reaching for microservices

A small application can look complicated long before it needs several independently deployed services. A checkout feature touches accounts, inventory, payment, and notifications, so an architecture proposal draws four boxes and assigns each a database. The diagram looks organized. The next implementation task now requires coordinating network errors, deployment order, credentials, and data consistency before a customer can buy anything. The immediate problem was unclear responsibility, but the proposed solution introduced distributed operations.

This article uses an illustrative ticketing application. Its team releases one product together, and its traffic can fit within one deployment. Those assumptions matter: a company with established independent teams and sharply different workloads may make another choice. The goal is to separate responsibilities at the lowest operational cost that meets the actual requirements.

Diagnose the reason for the split

Ask what cannot be accomplished with one deployment. Independent release schedules, incompatible runtime needs, regulatory separation, and a demonstrated scaling constraint are concrete answers. A folder containing many files is not. Neither is an expectation that the application might become popular. Write the proposed benefit as an observable statement: inventory must remain available when email delivery fails, or reporting must scale without increasing checkout capacity.

Then identify whether that benefit needs a process boundary. An email worker can be deployed separately while checkout and inventory remain modules in the same application. A report can use a bounded query or separate read model without making every business capability a service. Trace one request and count the new points at which it can partially fail. The split should solve a specific problem large enough to justify those additional states.

Give modules meaningful ownership

In the ticketing example, catalog owns event descriptions, inventory owns seat availability, and checkout owns purchase intent. Define their public operations in business terms. Checkout requests a reservation through inventory's interface; it does not update inventory tables through a convenient shared helper. Keep private implementation details behind that interface even though the code runs in one process.

Shared storage does not mean shared ownership of every record. Document which module can write each table or collection. Read access may use explicit query interfaces where useful. Avoid pretending local calls are network calls: adding serialization, asynchronous messaging, and generic request envelopes to every function can recreate service complexity inside the monolith. The boundary should make responsibilities visible while preserving straightforward local execution and ordinary debugging.

Start with one vertical change

Choose one change that crosses modules, such as reserving tickets during checkout. Define the input, the reservation result, and the failure that the user sees when seats are unavailable. Keep validation near the input boundary and inventory rules within inventory. Use a transaction where a genuinely atomic invariant requires it; do not add messaging simply because a diagram includes several boxes.

A useful implementation artifact is a boundary note with four entries: public operation, data owner, caller, and prohibited dependency. For example, reserve seats belongs to inventory, checkout may call it, and catalog must not call checkout. Put this note next to the module or in a short architecture document. Review imports when implementing the first change. If every module reaches into every other module's internal files, the names alone have not created modularity.

Make boundaries enforceable

Use the repository's existing tools to prevent the most damaging dependencies. Package exports, lint restrictions, separate namespaces, or focused dependency checks may be sufficient. Select the smallest mechanism that contributors can understand. A custom architecture framework is usually unnecessary for a few modules.

Tests should enter through public module operations and exercise relevant business behavior. A checkout test can verify that unavailable seats prevent confirmation without asserting the private layout of inventory files. Where a database is shared, check that writes still pass through the owning module. Code review should flag imports of private repositories and cross-module mutations. These checks preserve a possible future extraction, but extraction is not promised: data coupling and transaction requirements may still make a distributed boundary expensive.

Verify the operational benefit

Before declaring the design successful, implement and release a representative change. Record whether the team can locate the responsible rule, test it without the entire UI, and deploy without coordinating several versions. Measure application behavior under a realistic bounded workload if scale motivated the discussion. A module boundary does not reduce database contention by itself.

Also test failures that are meant to be isolated. If notification delivery is asynchronous, unavailable email should leave a visible pending or failed notification while the confirmed purchase remains durable. If checkout still synchronously waits for email, the desired isolation has not been achieved. Capture this as an acceptance case instead of relying on the architecture diagram. For detailed request mapping, see drawing a boundary from a user journey.

Recognize the cost you retain

A single deployment couples release timing and may allow one process failure to affect several capabilities. Shared database resources can also produce noisy neighbors. These are real limitations, not reasons to deny operational evidence. If inventory needs a separate availability objective and independent capacity, record that requirement and compare a worker, a resource boundary, and a service extraction.

Extraction becomes more defensible when the interface is stable, ownership is clear, and the team can operate multiple deployments. Plan authentication, retries, tracing, compatibility, and data migration before converting local calls into remote calls. The monolith is not automatically simple; unmanaged dependencies can make it hard to change. Its advantage is that you can repair and move boundaries without paying the network coordination cost on every experiment.

Use AI to challenge the decision

Give an AI assistant the actual deployment process, team ownership, data model, and measured constraints. Ask it to identify which requirements require independent processes and which can be satisfied by modules. Require each recommendation to name the evidence supporting it. An assistant should mark missing capacity measurements as unknown instead of inventing throughput or future team size.

A productive follow-up asks for a review of one boundary: list imports that violate ownership, propose a minimal interface, and describe failure cases that change if the interface becomes remote. Review those findings against files and tests. Avoid generating several services before validating the boundary. Keep the final choice in a short architecture decision, including what evidence would justify revisiting it.

Sources

Martin Fowler's Monolith First discusses the operational premium and uncertain early boundaries behind a monolith-first approach.

The C4 system context guide provides a primary reference for showing a system's relationship with people and external systems. The ticketing example and implementation checklist above are illustrative design guidance.

About the Author

Kamruz

Kamruz

Full Stack Developer

A passionate developer who loves creating modern web applications and sharing knowledge with the community.

More About Me

Related Articles