Microservices vs Monolith: Architecture Decision Guide
Make the right architecture choice. Covers when to use monolith, microservices, or modular monolith with decision criteria, trade-offs, migration strategies, and team size considerations.
The microservices vs monolith debate is usually framed wrong. It’s not about which is “better” — it’s about which is right for your team size, deployment needs, and system complexity right now. Most teams that start with microservices should have started with a monolith, and most monoliths that get extracted into microservices should have been modular monoliths first.
The hidden cost of microservices is not technical — it’s organizational. Every microservice needs its own CI/CD pipeline, its own monitoring, its own on-call rotation, and its own data store. If you have 5 engineers and 20 microservices, you have a distributed monolith with network calls instead of function calls: all the complexity, none of the benefits.
Three Architecture Options
| Monolith | Modular Monolith | Microservices | |
|---|---|---|---|
| Deployment | Single unit | Single unit, modular internals | Independent services |
| Team size | 1-15 engineers | 5-40 engineers | 20-500+ engineers |
| Complexity | Low | Medium | High |
| Data | Single database | Single DB with schema separation | Database per service |
| Communication | Function calls (in-process) | Module interfaces (in-process) | Network calls (HTTP, gRPC, events) |
| Failure mode | All up or all down | All up or all down | Partial failure (harder to debug) |
| Testing | Simple (one test suite) | Moderate (module boundaries) | Complex (contract tests, E2E) |
| Infrastructure tax | Low | Low | High (service mesh, gateway, bus) |
| Latency | In-process (ns) | In-process (ns) | Network (ms) |
The Monolith
┌─────────────────────────────────┐
│ Monolith │
│ ┌─────┐ ┌──────┐ ┌─────────┐ │
│ │Users│ │Orders│ │Payments │ │
│ └──┬──┘ └──┬───┘ └────┬────┘ │
│ └───────┴──────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ DB │ │
│ └─────────┘ │
└─────────────────────────────────┘
Strengths:
- Simple to develop, test, deploy, and debug — one codebase, one process
- Transaction consistency is trivial (ACID across all modules)
- Best performance: in-process function calls, no serialization overhead
- One CI/CD pipeline, one deployment artifact, one monitoring dashboard
- Refactoring is easy: IDE “rename symbol” works across the entire app
When to use:
- Team < 15 engineers (you don’t have the people to manage distributed systems)
- Early-stage product (still searching for product-market fit — you’ll rewrite anyway)
- Simple domain (< 10 bounded contexts)
- Need to ship fast and iterate quickly
- Budget doesn’t support infrastructure tax of microservices
Common failure: Monoliths fail when they become “big balls of mud” — no module boundaries, circular dependencies, and changes in one area break unrelated features. The fix is a modular monolith, not microservices.
The Modular Monolith (Often the Best Choice)
┌─────────────────────────────────────────┐
│ Modular Monolith │
│ ┌──────────┐ ┌──────────┐ │
│ │ Users │ │ Orders │ │
│ │ Module │→→│ Module │ │
│ │ (public │ │ (public │ │
│ │ API) │ │ API) │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ┌────┴──────────────┴────┐ │
│ │ Shared Database │ │
│ │ (schema per module) │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────┘
Module Communication Rules:
✅ ModuleA → ModuleB.PublicAPI (defined interface)
❌ ModuleA → ModuleB.InternalService (bypasses API)
❌ ModuleA → ModuleB.DatabaseTable (shared data coupling)
Strengths:
- Monolith deployment simplicity with module boundaries that enforce encapsulation
- Easy to extract modules into microservices later (boundaries already defined)
- Clear ownership model without network complexity
- Enforced module boundaries prevent the “big ball of mud” problem
- ACID transactions within the monolith (no distributed transactions needed)
How to enforce module boundaries:
project/
├── modules/
│ ├── users/
│ │ ├── public/ # Public API — other modules use this
│ │ │ └── UserService.ts
│ │ ├── internal/ # Private — only users module accesses this
│ │ │ ├── UserRepository.ts
│ │ │ └── PasswordHasher.ts
│ │ └── schema/ # Database schema owned by this module
│ │ └── users.sql
│ ├── orders/
│ │ ├── public/
│ │ └── internal/
│ └── payments/
│ ├── public/
│ └── internal/
└── shared/ # Truly shared utilities (logging, errors)
Microservices
┌────────┐ HTTP ┌─────────┐ gRPC ┌──────────┐
│ Users │────────→│ Orders │────────→│ Payments │
│Service │ │ │ Service │ │ Service │
└───┬────┘ │ └────┬────┘ └────┬─────┘
│ │ │ │
┌───┴─┐ ┌──┴──┐ ┌────┴───┐ ┌────┴───┐
│Users│ │Event│ │Orders │ │Payment │
│ DB │ │ Bus │ │ DB │ │ DB │
└─────┘ └─────┘ └────────┘ └────────┘
Strengths:
- Independent deployment: change one service without touching (or restarting) others
- Technology diversity: use Python for ML, Go for performance, Node for APIs
- Independent scaling: scale only what’s bottlenecked (scale Orders without scaling Users)
- Team autonomy: each team owns their service end-to-end (build, run, operate)
When to use:
- Team > 20 engineers where organizational scaling benefits matter
- Independent deployment is critical (multiple teams shipping multiple times per day)
- Different parts of the system need fundamentally different scaling profiles
- Domain boundaries are well-understood and stable (not still being discovered)
The infrastructure tax:
| Investment Required | What | Why |
|---|---|---|
| Service mesh (Istio, Linkerd) | mTLS, traffic management, retries | Services need secure, reliable communication |
| API gateway | Rate limiting, auth, routing | Single entry point for external traffic |
| Distributed tracing | Jaeger, Zipkin, Datadog APM | Debug requests that cross 5+ services |
| Event bus | Kafka, RabbitMQ, SQS | Asynchronous communication between services |
| CI/CD per service | N pipelines for N services | Each service deploys independently |
| Contract testing | Pact, Spring Cloud Contract | Verify service APIs don’t break consumers |
| Service catalog | Backstage, Port | Track what services exist and who owns them |
Decision Framework
Team size < 15?
└── ✅ Monolith (you don't have the people to run distributed systems)
Product-market fit found?
├── No → ✅ Monolith (you'll pivot and rewrite — optimize for speed)
└── Yes → Continue ↓
Domain boundaries clear and stable?
├── No → ✅ Modular monolith (discover boundaries first, extract later)
└── Yes → Continue ↓
Independent deployment truly needed?
├── No → ✅ Modular monolith (simpler, same internal benefits)
└── Yes → Continue ↓
Can you afford the infrastructure tax?
├── No → ✅ Modular monolith (extract only the service that needs independence)
└── Yes → ✅ Microservices (with platform team to manage complexity)
Migration Path: Monolith → Microservices
Don’t rewrite from scratch. Extract incrementally using the Strangler Fig pattern.
Step 1: Identify bounded contexts in your monolith (use event storming)
Step 2: Refactor into modular monolith (enforce module interfaces)
Step 3: Extract highest-value module as first microservice
→ Choose the module with the most independent scaling need
→ OR the most different technology requirement
Step 4: Add inter-service communication (API gateway + event bus)
Step 5: Extract remaining modules one at a time (months apart, not days)
Step 6: Eventually decommission monolith (but this may never happen — that's OK)
Common mistakes:
| Mistake | Problem | Fix |
|---|---|---|
| Extracting too many services at once | Can’t debug distributed failures | Extract 1 service, stabilize, then next |
| Shared database between services | Tight coupling, defeats the purpose | Each service gets its own database |
| Synchronous chains (A→B→C→D) | Latency compounds, any failure kills all | Use async events where possible |
| No observability before extraction | Can’t tell what went wrong or where | Deploy tracing + metrics BEFORE extracting |
| ”Nano-services” (too small) | 200 services for 10 developers | One service per bounded context, not per entity |
Team Size Decision Guide
| Team Size | Architecture | Rationale |
|---|---|---|
| 1-5 engineers | Monolith | Not enough people to own separate services |
| 5-15 engineers | Modular monolith | Clear module boundaries, shared deployment |
| 15-50 engineers | Selective microservices | Extract services for clear domain boundaries |
| 50+ engineers | Full microservices | Necessary for team autonomy and deployment independence |
Migration Path: Monolith to Microservices
The safest migration follows this strangler fig pattern:
- Add API gateway — Route traffic through a gateway in front of the monolith
- Extract one service — Choose the simplest, best-bounded domain (e.g., notifications, auth)
- Run in parallel — Both monolith and new service handle requests during transition
- Validate and cut over — Verify the extracted service works, redirect all traffic
- Repeat — Extract the next service, always starting with the least risky
Never rewrite all at once. Extract incrementally over 6-18 months.
Checklist
- Team size and 12-month growth trajectory assessed
- Domain boundaries mapped using event storming
- Data ownership per module/service defined (no shared databases)
- Communication patterns designed (sync for queries, async for commands/events)
- Infrastructure tax estimated (CI/CD, observability, service mesh, gateway)
- Starting architecture chosen (default recommendation: modular monolith)
- Module boundary enforcement established (if modular monolith)
- Migration plan documented with clear extraction order
- Observability in place before any extraction begins
- Contract testing strategy defined for service boundaries
:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For architecture consulting, visit garnetgrid.com. :::