Practice this topic in a realistic system design interview
Starting a new system as one deployable unit is usually the simplest choice. You have one codebase, one build, one deployment, and one place to debug. Code can call other code with normal function calls. The real question is how long that simplicity lasts as the system grows.
A monolithic architecture packages the whole application as one thing you deploy, even if the code inside is split into clean modules. It keeps development and operations simple, but as it grows, it needs clear boundaries, clear ownership, good tests, and careful releases.
Without that discipline, a monolith can slowly turn into a tangled codebase that is hard to change.
This chapter explains what makes an application a monolith, how layered and modular monoliths are structured, where they work well, where they become painful, and how to evolve one gradually.
A monolith is an application whose major features are packaged, deployed, and scaled together as one unit.
A monolith usually runs as one main application and is released as one unit.
The defining traits are:
A monolith does not mean one file, one folder, one team, or no architecture. A well-designed monolith has clear modules and ownership. A poorly designed monolith is just tangled code in one process.
Good monoliths are usually structured in two ways: by technical layer and by business area.
Horizontal layers separate technical responsibilities.
The table shows what each layer owns, with concrete examples.
| Layer | Responsibility | Examples |
|---|---|---|
| Presentation | Accept requests and return responses | Controllers, REST endpoints, views |
| Application | Coordinate use cases | Checkout flow, account update, refund workflow |
| Domain | Enforce business rules | Order, invoice, payment policy, inventory rule |
| Infrastructure | Talk to external systems | Database repositories, queues, payment provider clients |
Layering keeps technical concerns from spreading everywhere. It is helpful, but layering alone is not enough. Large systems also need boundaries around business areas.
Vertical modules group code by business capability.
A modular monolith keeps one deployment unit but protects boundaries between modules. Each module owns its business logic and exposes a small public interface. Other modules should not reach into its private classes, tables, or code details.
This is often the best middle ground for teams that want strong business boundaries without the extra work of running microservices.
A monolith handles a request with normal in-process calls until it reaches something outside the application, such as a database, cache, queue, or third-party API.
This gives a monolith several practical benefits:
Everything shares the same running process, though. A memory leak, slow dependency, overloaded thread pool, or bad deployment can affect the whole application.
A single deployable unit removes much of the coordination work that distributed systems carry.
Developers can often run the entire application locally.
This is especially valuable early in a product, when the business rules are still changing and teams need fast feedback.
With a monolith, the whole application ships as one package, so the release pipeline stays short. The diagram walks from source code to a single deployable unit.
There are fewer moving parts during release because one package goes out. Blue-green deployments, rolling deployments, and canaries are still useful, but the release unit is easier to understand.
When related data lives in one database, transactions are straightforward.
In a distributed system, the same workflow may require sagas, outbox tables, safe retry keys, compensating actions, and careful handling of temporary inconsistency.
A monolith usually means fewer deployable services, fewer dashboards and alerts, and fewer network calls. It also means fewer service-to-service authentication paths and fewer compatibility rules to maintain.
This matters. Many teams struggle with microservices because they take on distributed system complexity before they have the team size, tooling, or operating habits to support it.
A well-tested monolith is often easier to refactor than a distributed system. You can rename methods, move classes, update schemas, and adjust workflows in one coordinated commit.
Cross-service refactoring is slower because it often requires temporary compatibility rules, staged deployments, data migrations, and careful rollback plans.
Monoliths become painful when coupling grows faster than discipline.
A monolith usually scales as a whole.
If one workload needs more CPU, more memory, or different hardware, you usually scale the entire application unless you extract that workload or isolate it inside the monolith.
This is not always a problem. Many applications scale horizontally as monoliths for a long time. It becomes a problem when different parts of the application need very different amounts or types of resources.
Every release ships the whole application. A five-line change in user settings still goes out with orders, billing, reporting, and admin code.
That shared release path increases test cost and release risk. Strong automated tests, feature flags, canary deploys, and fast rollback reduce the risk, but they do not change the fact that the application ships as one unit.
One module can consume shared resources and hurt unrelated features. A memory leak in report generation can crash the process. A slow external API call can use up request threads. A bad database query can fill the shared connection pool. A CPU-heavy export job can slow down user-facing requests.
Bulkheads, timeouts, separate worker pools, queues, and rate limits help, but isolation is weaker than in independently deployed services.
Large monoliths can suffer from slow builds, slow tests, and long startup time. They can also make CI expensive, dependencies hard to understand, and onboarding harder for new developers.
These are engineering problems, not destiny. They require investment in build tooling, test strategy, dependency management, and module ownership.
A monolith tends to standardize on one main language, framework, runtime, and database approach. That consistency is useful, but it can make specialized tools harder to adopt.
For example, an AI-heavy recommendation pipeline may need Python libraries, GPU workers, vector indexing, or batch processing that does not fit cleanly inside the main application runtime.
The answer is not always "rewrite as microservices." Often the better choice is to keep the core monolith and extract only the specialized workload.
As the number of developers grows, coordination becomes harder.
Without clear ownership, teams get in each other's way through shared models, shared tables, shared utilities, and broad changes that cross business areas.
The danger is not the monolith itself. The danger is accidental coupling: code becoming connected in ways nobody planned.
The symptoms are familiar. Nobody knows who owns a module. Any module can call another module's private code. Database tables are updated from unrelated areas. Test failures are hard to connect to the change that caused them. Developers copy whatever pattern they can find because the correct path is unclear. Releases become slow because nobody knows what might break.
This pattern is preventable. It requires boundaries that are visible in code and protected by tools, tests, reviews, and ownership.
A monolith stays maintainable only if its internal structure is kept disciplined as it grows. Without that discipline, a large codebase steadily becomes harder to change.
Modules should communicate through clear public interfaces.
Useful techniques include package visibility, internal modules, architecture tests, and dependency rules enforced in CI. Assigning code owners by module, defining clear public APIs between modules, and setting rules for shared libraries all help keep those boundaries in place.
Prefer business-oriented modules over purely technical folders.
This makes ownership clearer and reduces the chance that every change touches generic services, models, or utils folders.
Even with one database, modules should own their tables or schemas.
This discipline makes future extraction possible. If every module reads and writes every table, turning one module into a service later becomes a data migration nightmare.
Not every workload belongs in the request path. A healthy monolith often uses background workers, queues, caches, search indexes, and specialized jobs.
For example, you can send emails through a queue, run report generation in workers, and process uploaded media after the request finishes. You can also generate embeddings outside the web request and use a search index for full-text search.
This keeps the monolith simple without forcing every feature to run inside the same user-facing request path.
A monolith needs a serious test strategy. Fast unit tests cover business logic. Focused integration tests cover database and framework behavior. Contract tests cover external APIs. End-to-end tests protect critical user journeys. Smart test selection keeps feedback fast in large codebases.
If every pull request waits hours for feedback, developers will batch changes, and releases will become riskier.
A monolith is often the right starting point. It works especially well when the team is small, the product is still changing, and service boundaries are not obvious yet.
| Indicator | Why a Monolith Works |
|---|---|
| New product | The domain will change; one codebase is easier to reshape. |
| Small team | Coordination work is low and distributed ownership is unnecessary. |
| Strong transactions | A single data store simplifies consistency. |
| Unclear boundaries | It is easier to discover boundaries before turning them into services. |
| Limited operations capacity | One deployable system is easier to run than many services. |
| Fast iteration matters | Fewer moving parts usually means faster early delivery. |
Choose a monolith when simplicity is the advantage. Add modularity early, but avoid splitting the system across the network before there is a clear reason.
A monolith starts to strain when one unit of scaling, building, and deploying no longer matches how the system and teams actually work.
| Warning Sign | What It Usually Means |
|---|---|
| One module needs different scaling | The workload may need isolation or extraction. |
| Builds and tests take too long | Tooling and module boundaries need investment. |
| Small changes cause unrelated failures | Internal coupling is too high. |
| Teams block each other on releases | Ownership and the release process no longer match. |
| One dependency constrains the whole app | Specialized workloads may need a separate runtime. |
| Database ownership is unclear | Future extraction will be expensive. |
Do not jump straight from these symptoms to microservices. First ask whether the monolith can be modularized, whether one workload can be extracted, or whether the build and deployment pipeline needs improvement.
The safest path is usually gradual.
The aim is to put boundaries where they create real value, not to escape the monolith just because it is a monolith.
Large engineering organizations have taken different paths, but a common lesson shows up repeatedly: architecture should follow the product, team, and operational reality.
These examples do not mean every company should copy their architecture. They show that monoliths can remain viable when teams invest in boundaries, performance, tests, and operations.
A monolith is one deployable application, not necessarily one messy codebase. It is often the right starting point because it avoids the operational complexity of distributed systems. A modular monolith gives you business boundaries without independent service deployment.
The main risks are hidden coupling, failures that affect the whole running app, slow builds, unclear ownership, and shared releases. Do not split a monolith just because it is large. Split when a boundary has clear scaling, reliability, ownership, or technology reasons. A well-maintained monolith is better than a poorly designed microservice system.
10 quizzes