Back to blog

Software architecture

What Is a Modular Monolith? Building Clear Boundaries Before Microservices

Learn how a modular monolith separates business capabilities, data and contracts while keeping one application and a simpler deployment model.

A modular monolith keeps one application and one deployment while separating its business capabilities through explicit ownership and controlled interfaces. It can make growing software easier to change without adding the operating cost of microservices before that cost is justified.

01

The application is still one system, but every change touches everything

Imagine a company runs an installer operations platform. It registers installers, accepts jobs, collects evidence, reviews submissions, issues certificates and sends approved charges to finance.

The first version was easy to follow. A small team added features quickly, and keeping everything in one application made sense. Over time, the folders stayed tidy but the responsibilities did not. A change to certificate delivery now reaches installer records, job status, finance data and reporting. A controller can update tables belonging to three different parts of the operation.

The business feels the coupling before anybody gives it an architectural name. Small changes take longer to estimate. Developers are nervous about touching familiar code. Tests far away from the change begin to fail. A person working on finance needs to understand how evidence review happens because both areas use the same models and tables.

Microservices can look like the escape route. Split the application into separate services and the boundaries will surely become clear. Unfortunately, unclear boundaries do not become good boundaries because they now communicate over a network. The team can preserve the same coupling and add API versions, queues, retries, timeouts and several deployment pipelines.

02

A modular monolith separates code without separating the deployment

A monolith describes how an application is built and deployed. It does not automatically describe the quality of the code inside it. One deployable application can be carefully separated, just as a collection of services can be tightly coupled and difficult to change.

A modular monolith keeps the operational simplicity of one application while dividing the software around coherent business capabilities. For the installer platform, sensible candidates might be Installer Management, Job Delivery, Evidence Review, Certification, Finance and Reporting.

Each module owns a set of decisions and the data needed to make them. Other modules use an explicit contract when they need something from it. They do not reach into its private models, update its tables or depend on an accidental relationship hidden in the database.

The useful test is not whether the repository contains a folder called `Modules`. Ask whether one capability can change without somebody needing to understand and edit all the others. If the answer is still no, the files have moved but the architecture has not.

03

Draw the boundaries around business responsibility

Technical folders such as Controllers, Models, Jobs and Services are normal framework conventions. They tell a developer what kind of class they are looking at. They do not say which part of the business owns the behaviour.

When all controllers live together and all models live together, one workflow becomes scattered across the repository. To understand evidence approval, a developer jumps between a controller, service, model, policy, event and several listeners. The structure is orderly, but the business capability is not visible in one place.

A module should have a name people in the operation recognise. Evidence Review can own the rules for accepting, querying and rejecting submitted evidence. Certification can own when a completed review becomes a certificate. Finance can own charges, statements and payment status. Reporting can own views designed for analysis rather than changing operational records.

For each candidate module, write down four things: the decisions it owns, the information it may change, the operations other modules may request and the facts it publishes after important work completes. If those answers are vague, more discovery is needed before the folder structure is changed.

04

A boundary is real when the module owns its data and rules

Names and namespaces help people navigate the code, but they cannot stop a shortcut. Real separation needs ownership rules that the team can explain and check.

Evidence Review may store an installer identifier, but that does not give it permission to update the installer account. It can ask Installer Management for the current status through a small query contract. It can publish an `EvidenceApproved` fact after its own transaction completes. It should not return a writable installer model or expose a query builder that lets callers inspect every internal column.

This discipline can work inside one relational database. Separate database servers are not required. Tables can be grouped by naming convention or schema, while code review and automated checks ensure that one module does not write another module's data. A foreign key may still be worthwhile when referential integrity matters more than independent schema evolution. The trade-off should be deliberate.

Data ownership is often the point where a proposed module boundary proves useful or falls apart. Two areas that must update the same records inside the same business rule may belong together. Forcing them apart can create coordination work without creating independence.

05

Use explicit contracts instead of reaching through the back door

A public module contract should describe a business operation, not reveal the implementation. Certification might offer an operation to issue a certificate from an approved review. The caller supplies a small command and receives a certificate identifier or a clear business error.

The contract should not return the module's internal database model. A model carries relationships, persistence methods, column knowledge and writable state. Once another module depends on it, a private implementation has quietly become a public API.

Small data transfer objects can provide the information needed for one known purpose. Finance might ask Certification for a certificate status and completion date. Certification remains free to rename columns or change how that status is calculated as long as it keeps the contract.

Contracts do create code, and not every class needs an interface. Use them where a business boundary is crossed. Inside a module, straightforward framework code may remain the clearest option. The goal is not to recreate an enterprise architecture textbook inside every feature.

06

Events are useful when they describe completed facts

Some work should happen after a module has completed its own responsibility. When evidence is approved, Certification may become eligible to issue a document and Reporting may update a review summary. An event can publish the completed fact without allowing either listener to control the Evidence Review transaction.

The wording matters. `EvidenceApproved` describes something that happened. `ApproveEvidenceRequested` asks somebody else to complete part of the current job and can hide who owns the result. Direct calls are normally clearer when the caller needs an immediate answer, validation error or guarantee.

Laravel can dispatch an event after a database transaction commits. That prevents listeners reacting to a change that later rolls back. It does not, by itself, guarantee durable delivery after the commit. Important projections or integrations may still need an outbox, retries, idempotent listeners and a way to rebuild missing read data.

Events can also make a simple application hard to trace when everything becomes a broadcast. Use them for stable facts and independent reactions. Keep commands and required answers explicit.

07

One deployment keeps the operating model simpler

A modular monolith can still use one repository, one release process and one main application environment. Developers can run the complete workflow locally. A release can change two modules together when the business needs it. A normal database transaction can protect a rule that genuinely belongs inside one owner.

That simplicity has business value. There are fewer deployed components to secure, monitor and recover. A support engineer can follow one request through one process instead of correlating logs across several services. A small team is not required to build a platform engineering function before it can improve the product.

The architecture still needs discipline. A shared deployment can tempt developers to call any class or table because everything is nearby. That is why internal dependency rules matter. The modules must remain separate by design even though the runtime makes shortcuts technically possible.

This is the middle ground the usual monolith versus microservices debate misses. The application can have clear internal ownership without forcing each area to become a separately operated system.

08

Testing should prove the boundaries, not only the behaviour

Feature tests should check that each public contract keeps its promise. Can an approved review produce a certificate? Does a rejected review leave finance untouched? Does replaying a reporting event create duplicate rows? These tests make the module useful, not merely well organised.

Architecture tests can check dependency direction. They can reject an import of a private Certification class from Finance or stop Reporting from using an internal Evidence Review model. The build fails while the shortcut is still cheap to correct.

Automated tests cannot see every possible leak. Raw SQL, shared cache keys and undocumented external calls can bypass a perfect namespace rule. Code review and database checks still matter. The aim is to make the easy mistake visible and the intended route easier to follow.

A boundary is strongest when a new developer can discover it from the code, a reviewer can explain it and the test suite can reject the most common way it would be broken.

09

Compare three shapes before choosing the next step

The useful choice is not always between one unstructured application and microservices. A modular monolith is often the sensible way to improve ownership while keeping delivery and support manageable.

Three application shapes compared
Decision areaLayered monolith with weak boundariesModular monolithMicroservices
Business ownershipOften hidden across technical foldersExplicit inside modulesExplicit across independently owned services
DeploymentOne applicationOne applicationSeveral independent deployments
DataShared models and tables are easy to reachOwnership rules inside one database or controlled schemasEach service normally owns its data
CommunicationDirect calls and shared internalsPublic contracts and deliberate eventsNetwork APIs and messages
Failure handlingMostly in-processMostly in-process, with selected asynchronous workPartial network failure must be expected
Operating costLow at first, but change can become riskyModerate design discipline with a simpler runtimeHigher platform, monitoring and support responsibility
Best fitSmall, simple or short-lived applicationsGrowing systems that need clearer change boundariesCapabilities with proven reasons to operate independently

10

Improve one capability without rewriting the system

Turning an existing Laravel application into a modular monolith should be an incremental refactor. A rewrite can hide the same misunderstandings inside new code and delay useful business work for months.

Choose one capability with recognisable rules and visible pain. Map every controller, job, command, model, table and integration involved. Search for the places outside that area which read or write its data. The inconvenient dependencies are the most valuable part of this exercise.

Define ownership before moving files. Put one important use case behind a public contract, then move that vertical slice with its behaviour and tests. Replace foreign model access with focused queries or completed events. Once a dependency has been removed, add an architecture test that stops it returning.

The application should remain deployable throughout. Repeat the process around real use cases rather than creating empty modules and abstractions for work that may never arrive. The aim is safer change, not a more impressive directory tree.

11

Microservices should solve an operational constraint

A well separated module can become a service later, but extraction is not the definition of success. Many systems can remain modular monoliths for years because one deployment continues to fit the team and the operation.

Separation starts earning its cost when a capability needs independent scaling, release control, security, availability or ownership. Document conversion may consume far more computing capacity than the rest of the platform. A payments capability may require a stricter release process. Several teams may need to change different areas without coordinating one production window.

At that point, the modular boundary provides evidence. The team already knows what the capability owns, which contracts callers use, what data must move and which guarantees will change when an in-process call becomes a network interaction.

Do not split a service only because a module has become large. Ask what needs to become operationally independent and whether the team can support the extra deployment, monitoring, security and failure handling. If the answer is vague, keep the module inside the application and improve the boundary there.

12

Start by making ownership visible

The installer platform does not need six new services to become easier to change. It needs the Evidence Review code to stop editing installer accounts, the Finance code to stop querying every operational table and the Reporting area to stop acting as a route around every rule.

Start with the capability causing the most friction. Agree its owner, data, public operations and published facts. Put one valuable workflow through that boundary and test it. The result should be a change that is easier to understand and safer to release, not simply more files.

There is a cost to modularity. The team must maintain contracts, decide where data belongs and resist convenient shortcuts. Small duplication can be cheaper than a shared abstraction that couples unrelated rules. The architecture should stay proportionate to the product and the people who support it.

I help businesses review software structure, delivery constraints and operational risk before committing to a rebuild or microservices programme. The first useful question is usually not how many services to create. It is which part of the system owns the decision that has become difficult to change.

Useful questions

Before calling a monolith modular, ask:

  • Can each module be named as a business capability rather than a technical layer?
  • Are the decisions and rules owned by each module written down?
  • Is one module prevented from writing another module's data?
  • Do cross-module calls use a deliberate public contract?
  • Do public contracts return small purpose-built values rather than internal models?
  • Are events used for completed facts instead of hiding required commands?
  • Can architecture tests reject the most common boundary violations?
  • Can one use case be moved behind a boundary without rewriting the application?
  • Which capability has a proven reason to deploy, scale or fail independently?
  • Can the team support the extra operating cost before extracting a service?
Explore business software consultancy
Daniel Mills

Written by Daniel Mills

Business understanding and hands-on software delivery.

I help owners and teams improve the software they rely on, replace fragile processes and turn new ideas into practical systems people can actually use.