A monolith often gets a bad reputation in modern software development. As applications grow, teams are frequently told that they should move toward microservices, separate their frontend and backend, introduce message queues, or break the application into independently deployable services.
But a monolith isn't inherently a bad architecture.
In fact, many successful applications begin as monoliths for good reasons. A single application is easier to develop, deploy, debug, and understand when a product is still evolving. The real problem usually appears later, when the application grows without clear boundaries and every part of the system becomes dependent on every other part.
At that point, the issue isn't simply that the application is a monolith.
The issue is that the application has become tightly coupled.
Why Monoliths Are Often the Right Starting Point
When a new product is being built, simplicity has enormous value.
A single application can contain the frontend, backend logic, authentication, database access, administration features, and business workflows without requiring a large collection of infrastructure. Developers can work within one repository, deploy one application, and debug problems without tracing requests across multiple independent services.
For a small or early-stage product, this can be exactly what the team needs.
There is little benefit in introducing distributed systems complexity when the product itself is still changing rapidly. A microservices architecture can introduce service discovery, network communication, independent deployments, monitoring, distributed logging, failure handling, authentication between services, and data consistency concerns.
Those problems are real, but a small application may not need them yet.
Starting with a monolith can therefore be a deliberate architectural decision rather than a shortcut.
When a Monolith Starts Becoming a Problem
The problems usually appear as the application grows.
A feature that once required a few files now touches several unrelated parts of the system. Developers become hesitant to make changes because they don't know what else might break.
A simple change to an order workflow might affect inventory, payments, notifications, reporting, and customer accounts. A database migration might require changes across multiple modules. An API modification might unexpectedly break an internal feature that depends on the same business logic.
Development slows down not because the application has reached a particular number of lines of code, but because the relationships between its parts have become difficult to understand.
This is an important distinction.
Size alone doesn't make a monolith unhealthy.
Complexity and coupling do.
The Real Enemy Is Tight Coupling
Imagine an application where the customer module directly accesses payment tables, the reporting module directly modifies order data, the notification system depends on controller logic, and administrative features reuse database queries from unrelated parts of the application.
Everything works.
Until something changes.
Now a developer wants to modify the payment workflow. They have to understand how customers, orders, reporting, notifications, and possibly several other modules interact with it.
The problem isn't that all of these capabilities exist in one application.
The problem is that their responsibilities aren't clearly separated.
A healthy architecture establishes boundaries between responsibilities even when everything still lives in the same codebase.
The goal should be to make each part of the system understandable and predictable.
Business Logic Should Have Clear Boundaries
One of the first signs of a growing monolith is business logic spreading everywhere.
Controllers start containing business rules. Database models begin performing unrelated operations. Utility functions accumulate application-specific behaviour. Frontend code starts making assumptions about backend implementation details.
Over time, the system becomes difficult to reason about because there is no obvious place where a particular business rule belongs.
A better approach is to establish clear boundaries around business capabilities.
For example, an e-commerce application might have distinct areas for:
Orders
Responsible for creating, updating, and managing orders.
Payments
Responsible for payment processing and transaction state.
Inventory
Responsible for stock availability and inventory changes.
Customers
Responsible for customer information and account behaviour.
Notifications
Responsible for communicating events to customers and internal teams.
These modules can still exist inside the same application.
The important part is that their responsibilities and interactions are explicit.
Database Coupling Can Become a Hidden Problem
The database is often where monolithic applications become most tightly coupled.
Multiple parts of the application may directly read and modify the same tables. At first, this feels convenient because everything is accessible.
Later, it becomes difficult to change anything safely.
A developer might discover that a column cannot be renamed because several unrelated modules depend on it. A change to one table may require updates across reports, APIs, background jobs, and administrative interfaces.
The database gradually becomes the contract connecting every part of the application.
This doesn't mean every module needs its own database.
In many cases, that would introduce unnecessary complexity.
Instead, the application should establish clear ownership of data and define how other modules interact with that data.
A modular application can share a database while still maintaining strong boundaries around how that database is accessed.
The Deployment Problem
A monolith also becomes difficult when every change requires deploying the entire application.
Imagine a large system containing a customer portal, payment processing, reporting, administration, notifications, and internal tools.
A developer makes a small change to the reporting module.
Because everything is packaged together, the entire application needs to be tested and deployed.
This can create slower release cycles and greater risk.
Teams may start avoiding frequent deployments because each deployment feels dangerous. Eventually, releases become larger, less frequent, and more difficult to validate.
This is often where teams begin considering independently deployable services.
But deployment problems don't always require microservices.
A well-structured modular monolith can often improve deployment and development workflows considerably before the system needs to be distributed.
Why Microservices Aren't the Automatic Answer
When a monolith becomes difficult to maintain, microservices can look like the obvious solution.
Split the application into services.
Put each service in its own repository.
Give each service its own database.
Deploy everything independently.
It sounds clean.
The reality is more complicated.
Once an application becomes distributed, communication happens over networks. Networks fail. Requests time out. Services become unavailable. Data consistency becomes harder. Debugging requires distributed tracing and centralized logging. Authentication becomes more complicated. Deployment and infrastructure requirements increase.
A function call inside a monolith is relatively straightforward.
A network call between two services introduces an entirely different class of failure.
Microservices solve certain organizational and architectural problems, but they also create new ones.
Moving to microservices simply because a codebase is large can therefore make an already complicated system even harder to operate.
The Modular Monolith
There is an important middle ground between a tightly coupled monolith and a distributed microservices architecture.
It is the modular monolith.
A modular monolith remains a single deployable application, but its internal architecture is divided into clearly defined modules.
Each module has a specific responsibility.
Each module exposes a controlled interface.
Other modules should interact through those interfaces rather than reaching directly into internal implementation details.
For example:
Orders → Payments
The order module requests payment processing through a defined payment interface.
It shouldn't need to know how the payment provider works internally.
Similarly:
Orders → Notifications
The order system can trigger a notification through a defined mechanism without directly depending on the notification implementation.
This creates architectural boundaries without immediately introducing distributed infrastructure.
A Modular Monolith Can Grow Surprisingly Far
There is sometimes an assumption that a modular monolith is only a temporary architecture before moving to microservices.
That isn't necessarily true.
A well-designed modular monolith can support significant complexity while keeping operational overhead relatively low.
It provides clear internal boundaries while retaining the simplicity of a single application.
Developers can work within one codebase. Deployment remains straightforward. Local development doesn't require running dozens of services. Transactions can remain simpler when modules share a database. Debugging can remain much easier than in a distributed environment.
And if a particular module eventually needs to become an independent service, a well-defined boundary makes that transition easier.
The architecture doesn't need to predict the future.
It needs to make future change possible.
When Should a Module Become a Service?
There is no universal number of users, developers, or lines of code that determines when a module should become a separate service.
The decision should come from actual architectural or organizational pressure.
A module may be a good candidate for separation when it needs to scale independently, has a significantly different deployment lifecycle, requires different infrastructure, or is maintained by a team that needs greater autonomy.
For example, an image-processing service might require very different computing resources from the main web application. A search system might need specialized infrastructure. A payment capability may require strong isolation and independent security controls.
These are meaningful reasons to introduce a separate service.
Simply having a large codebase isn't enough.
Architecture Should Follow Complexity
One of the biggest mistakes in software architecture is designing for an imaginary future.
A team may assume that the application will eventually have millions of users, hundreds of developers, and dozens of integrations.
So they build the infrastructure for that hypothetical future before the product has even found its market.
This creates unnecessary complexity.
The opposite mistake is also common: keeping everything tightly coupled long after the application has outgrown its original architecture.
Good architecture sits between these extremes.
Build for today's real requirements while creating boundaries that make tomorrow's changes easier.
That might mean starting with a monolith, introducing modular boundaries as the application grows, and extracting individual services only when there is a clear reason to do so.
Architecture can evolve.
It doesn't need to be perfect on day one.
The Importance of Ownership
Clear ownership is one of the simplest ways to improve a growing monolith.
If the application has an order module, someone should be able to answer what the order module owns.
What data does it control?
What operations does it expose?
What other modules can call it?
What assumptions does it make?
What events does it produce?
What dependencies does it have?
These questions create boundaries.
Without ownership, modules tend to reach into each other because everything is technically accessible.
The result is an application where developers can technically change anything but are afraid to change anything.
Good architecture reverses that situation.
Developers should have freedom inside a module and clear rules at its boundaries.
Refactoring Doesn't Mean Rewriting Everything
When developers realise that a monolith has become difficult to maintain, the first instinct can be to start again.
A complete rewrite often sounds attractive because the existing system has accumulated years of technical debt.
But rewriting a large application is expensive and risky.
The existing system contains business rules that may not be documented anywhere else. It contains edge cases discovered through real users, integrations that depend on specific behaviour, and operational knowledge accumulated over time.
A better approach is often incremental refactoring.
Identify a problematic area.
Define its responsibility.
Move related logic into a clear module.
Create a controlled interface.
Reduce direct dependencies.
Improve tests around the boundary.
Then repeat the process.
Over time, the monolith becomes more modular without requiring the business to stop and rebuild the entire product.
The Architecture Should Make Change Safer
The real purpose of architecture isn't to make diagrams look impressive.
It's to make change safer.
A good architecture allows a developer to modify one part of a system without needing to understand the entire application.
It makes dependencies visible.
It makes responsibilities clear.
It makes testing easier.
It makes failures easier to isolate.
It allows teams to work independently where appropriate.
And when the system eventually needs to be distributed, it provides meaningful boundaries from which services can be extracted.
This is why architectural boundaries matter more than architectural fashion.
Whether the application is a monolith, modular monolith, or collection of microservices is less important than whether its responsibilities are clearly defined.
Monolith vs Modular Monolith vs Microservices
The three approaches solve different problems.
A traditional monolith prioritizes simplicity. Everything lives inside one application, often with significant freedom for different parts of the codebase to interact.
A modular monolith keeps the simplicity of one application while introducing explicit internal boundaries between business capabilities.
Microservices take those boundaries further by making selected capabilities independently deployable and operationally separate.
None of these approaches is universally superior.
The right choice depends on the size of the product, team structure, operational requirements, scaling needs, deployment requirements, and complexity of the business domain.
The mistake is treating architecture as a competition where one model must always win.
Final Thoughts
Monoliths aren't inherently bad.
They become difficult when their internal boundaries disappear and every part of the application becomes dependent on every other part.
A well-structured monolith can be simpler to build, deploy, test, and operate than a distributed architecture. As complexity grows, introducing clear modules can provide many of the benefits of stronger separation without immediately taking on the operational cost of microservices.
Eventually, some modules may need to become independent services. When that happens, the transition is much easier if the boundaries already exist.
The most important architectural decision isn't whether your application has one repository or fifty services.
It's whether you know where one responsibility ends and another begins.
Good architecture isn't about having the most services.
It's about having the right boundaries.
Share this article



