1. The Monolith: What It Actually Is
A monolith is a single deployable unit that contains all your application's functionality. Your user authentication, business logic, data access, background jobs, and API endpoints all live in one codebase and deploy as one artifact.
"Monolith" has become a pejorative term in some circles, but that framing misses the point. Monoliths are not inherently bad. Some of the most successful software products in history -- including early versions of Amazon, Netflix, and Shopify -- started as monoliths and thrived for years before any decomposition was needed.
Strengths of a Monolith
- Simple development: One codebase, one IDE, one build process. A new developer can clone one repository and have the entire system running locally.
- Simple deployment: Deploy one thing. If it works, it works everywhere. Rollback means reverting one artifact.
- Simple debugging: A stack trace shows you the full call path. You can set a breakpoint and step through the entire request flow.
- No network overhead: Function calls are nanoseconds. HTTP calls between services are milliseconds at best. That 1000x difference compounds.
- ACID transactions: Your database gives you transactions for free. No distributed saga coordination needed.
- Refactoring is safe: IDE refactoring tools work across the entire codebase. Rename a function and every caller updates.
When Monoliths Struggle
- Team scaling: When 15 or more developers work in the same codebase, merge conflicts and coordination overhead increase significantly.
- Selective scaling: If one feature needs 100x more compute than others, you have to scale the entire application.
- Technology lock-in: The entire application must use the same language, framework, and runtime.
- Build times: As the codebase grows, compile and test times can reach 15-30 minutes, killing developer productivity.
2. Microservices: What They Actually Are
Microservices are an architectural style where an application is composed of small, independently deployable services, each running its own process and communicating through lightweight mechanisms (typically HTTP APIs or message queues).
Each service owns its data, has its own deployment pipeline, and can be written in whatever language best fits the problem. The key word is independently deployable -- if you cannot deploy a service without coordinating with other services, you do not have microservices.
Characteristics of True Microservices
- Single responsibility: Each service does one thing well, aligned with a business capability.
- Own data store: No shared database. Each service manages its own data and exposes it through APIs.
- Independent deployment: Updating one service does not require updating or restarting others.
- Decentralized governance: Teams can choose their own technology stacks, testing strategies, and deployment schedules.
- Designed for failure: Services assume other services can fail and handle that gracefully (circuit breakers, retries, fallbacks).
3. The Real Trade-Offs
The decision between these architectures is not abstract. Here are the concrete trade-offs that matter in production.
| Factor | Monolith | Microservices |
|---|---|---|
| Initial velocity | Faster. One codebase, one deploy. | Slower. Infrastructure overhead from day one. |
| Team scaling | Conflicts grow with team size. | Teams work independently on separate services. |
| Deployment risk | One deploy = entire app. Higher blast radius. | Small, targeted deployments. Lower blast radius. |
| Debugging | Stack traces, breakpoints, local debugging. | Distributed tracing, log aggregation, correlating across services. |
| Data consistency | ACID transactions in one database. | Eventual consistency, sagas, compensating transactions. |
| Infrastructure cost | Lower. One application to run. | Higher. Multiple services, databases, message brokers, service mesh. |
| Technology flexibility | One stack for everything. | Best tool for each job. |
| Scaling granularity | Scale everything together. | Scale individual services independently. |
| Operational complexity | Low. Standard monitoring and logging. | High. Service discovery, load balancing, container orchestration. |
When designing service contracts and API schemas, use the QTool JSON Formatter to validate and pretty-print the JSON payloads your services exchange.
4. The Distributed Monolith Antipattern
The distributed monolith is the worst outcome of a microservices migration. You pay the full complexity cost of distributed systems while gaining none of the benefits.
How to Recognize It
- Shared database: Multiple services read from and write to the same database tables.
- Coordinated deploys: You cannot deploy service A without also deploying service B.
- Synchronous call chains: Service A calls B, which calls C, which calls D. If D is slow, everything is slow.
- Shared libraries with business logic: Core business rules live in a shared library that every service depends on. Updating it means updating everything.
- One team owns everything: If the same 3 developers maintain all 12 services, you have a monolith with extra network hops.
Ask yourself: "Can I deploy this service independently, at any time, without telling anyone?" If the answer is no, you have a distributed monolith.
A Concrete Example
// Order Service -- directly queries user table
const user = await db.query(
'SELECT * FROM users WHERE id = ?', [userId]
);
// User Service -- also queries the same user table
const user = await db.query(
'SELECT * FROM users WHERE id = ?', [userId]
);
// Problem: Both services depend on the same table schema.
// Changing the users table requires coordinating both services.
// This is NOT microservices. This is a monolith with HTTP calls.
// Order Service -- calls User Service API
const user = await fetch(
`${USER_SERVICE_URL}/api/users/${userId}`
).then(r => r.json());
// User Service -- owns its database exclusively
app.get('/api/users/:id', async (req, res) => {
const user = await usersDb.findById(req.params.id);
res.json({
id: user.id,
name: user.name,
email: user.email
});
});
// User Service can change its database schema freely.
// Order Service only depends on the API contract.
5. The Modular Monolith: A Middle Ground
A modular monolith is a single deployable application that is internally organized into well-defined modules with clear boundaries. Each module owns its data and communicates with other modules through internal APIs -- not by reaching into shared database tables.
This approach gives you the deployment simplicity of a monolith with the organizational discipline that makes future extraction into microservices straightforward.
src/
modules/
users/
api/ # Public interface for other modules
UserService.ts
types.ts
internal/ # Private implementation
UserRepository.ts
UserController.ts
migrations/
tests/
orders/
api/
OrderService.ts
types.ts
internal/
OrderRepository.ts
OrderController.ts
migrations/
tests/
payments/
api/
PaymentService.ts
internal/
...
shared/ # True shared infrastructure only
database.ts
logger.ts
middleware.ts
The rule is simple: modules only import from other modules' api/ directories. If you enforce this with linting rules or architecture tests, you maintain clean boundaries while keeping the simplicity of a single deployment.
If you are starting a new project, build a modular monolith. It is the fastest way to ship and the easiest path to microservices later if you need them. Shopify ran a monolith until 2016 -- with thousands of developers and billions in revenue.
6. When Microservices Make Sense
Microservices solve organizational and scaling problems. If you do not have those problems, you do not need microservices. Here are the signals that indicate you might:
- 3+ teams working on the same product and stepping on each other's code.
- Vastly different scaling needs: Your search service handles 10,000 req/s while your billing service handles 10 req/s. Scaling them together wastes resources.
- Different technology requirements: Your ML pipeline needs Python, your API is in Go, and your real-time features need Elixir.
- Build times exceeding 10-15 minutes and developer complaints are increasing.
- Deployment frequency is limited because changes in one area require regression testing the entire system.
- Organizational growth: You are hiring fast and need teams to work autonomously.
Prerequisites for Microservices
Before adopting microservices, ensure your team has:
- Automated CI/CD pipelines (one per service)
- Container orchestration (Kubernetes or equivalent)
- Centralized logging and monitoring (ELK, Datadog, etc.)
- Distributed tracing (Jaeger, Zipkin, or OpenTelemetry)
- Service discovery and load balancing
- A clear domain model with identified bounded contexts
If you do not have these capabilities, adopting microservices will slow you down, not speed you up.
7. When a Monolith Makes Sense
- Small team (1-8 developers): The coordination overhead of microservices outweighs the benefits.
- New product / startup: You are still discovering what the product is. Premature decomposition creates the wrong boundaries.
- Tight deadlines: A monolith ships faster. Period.
- Simple domain: If your application is a straightforward CRUD app, microservices add complexity without benefit.
- Limited DevOps capability: If you do not have CI/CD, monitoring, and container orchestration, microservices will be a constant source of operational pain.
"If you can't build a well-structured monolith, what makes you think microservices is the answer?" -- Simon Brown
8. Migration Strategies
If your monolith has genuinely outgrown its architecture, here is how to migrate without a risky big-bang rewrite.
The Strangler Fig Pattern
Named after the strangler fig tree that grows around a host tree, gradually replacing it. You build new functionality as microservices while progressively migrating existing functionality out of the monolith.
- Place an API gateway in front of both the monolith and new services.
- Identify your first extraction target: Choose the component with the clearest boundaries and highest value for independent deployment.
- Build the new service alongside the monolith. Route traffic to the new service through the gateway.
- Migrate data from the shared database to the service's own database.
- Switch traffic from the monolith endpoint to the new service. Keep the monolith code as a fallback.
- Remove the old code from the monolith once the new service is stable.
- Repeat for the next component.
What to Extract First
Pick a component that is:
- Loosely coupled: Few dependencies on other parts of the monolith.
- Independently valuable: Deploying it separately provides a clear benefit (scaling, team autonomy, technology choice).
- Well-bounded: The data it owns is clear and does not overlap much with other domains.
- Not a core critical path: Start with a supporting service, not your payment processing.
Generate your Docker Compose configuration for the new service architecture with the Docker Compose Generator, and convert between JSON and YAML service configs with the YAML/JSON Converter.
Most monolith-to-microservices migrations take 1-3 years for medium-sized systems. Plan for a long coexistence period where both architectures run side by side. Do not set a deadline for "killing the monolith" -- let it shrink naturally as services prove themselves.
9. Frequently Asked Questions
What is the distributed monolith antipattern?
A distributed monolith is a system that has the deployment complexity of microservices but none of the benefits. Services are tightly coupled through shared databases, synchronous call chains, or coordinated deployments. You cannot deploy one service without deploying others, you cannot scale them independently, and a failure in one service cascades to others. It combines the worst aspects of both architectures: the complexity of distributed systems with the rigidity of a monolith.
When should you use microservices instead of a monolith?
Consider microservices when you have multiple teams (typically 3 or more) that need to deploy independently, when different parts of your system have vastly different scaling requirements, when you need to use different technology stacks for different components, or when your monolith has become so large that build times exceed 10-15 minutes and developer productivity is declining. Do not adopt microservices for a new project with a small team -- start with a well-structured monolith and extract services when you have clear evidence they are needed.
How do you migrate from a monolith to microservices?
The recommended approach is the Strangler Fig pattern: gradually replace pieces of the monolith with new services rather than rewriting everything at once. Start by identifying bounded contexts using domain-driven design. Extract the least coupled, most independently valuable component first. Route traffic through an API gateway that can direct requests to either the monolith or the new service. Keep the monolith running alongside new services during the transition. Each extraction should be a self-contained project that delivers value on its own.
What are the main disadvantages of microservices?
The main disadvantages include increased operational complexity (you need service discovery, load balancing, distributed tracing, and centralized logging), network latency between services, data consistency challenges (distributed transactions are hard), more difficult debugging and testing, higher infrastructure costs from running multiple services and their dependencies, and the need for a mature DevOps culture with CI/CD pipelines and container orchestration. For small teams, the overhead often outweighs the benefits.
What is the modular monolith and is it a good middle ground?
A modular monolith is a single deployable application that is internally organized into well-defined modules with clear boundaries and interfaces. Each module owns its data and communicates with other modules through defined APIs, not by reaching into shared database tables. It gives you the simplicity of a single deployment with many of the organizational benefits of microservices. It is an excellent middle ground and a recommended starting point because you can later extract modules into separate services if needed, with much less effort than untangling a traditional monolith.