The architecture debate never dies because people keep comparing best cases. Monolith fans show a clean Django app. Microservices fans show Netflix. Serverless fans show a Lambda that costs $0.02/month. Nobody shows the worst case.
Let me show you all three worst cases. Then you decide.
One codebase, one database, one deployment. For a small team, that's the simplest way to build and ship quickly.
app/
├── users/
├── billing/
├── orders/
├── notifications/
└── main.py
Deploy takes 3 minutes. One person can understand the whole system. grep works across the entire codebase. Debugging is a stack trace, not a treasure hunt across 12 services.
The problem arises when the codebase grows. A tiny fix in the cart code requires redeploying the entire app. One bad release takes down everything. The test suite takes 45 minutes. Every merge is a conflict lottery.
But here's what nobody says: most teams never hit this problem. You need 20+ engineers committing to the same repo daily before monolith coordination pain actually exceeds microservices operational pain.
Product, Cart, and Order each run on their own, scale separately, and manage their own data. You can ship changes to Cart without affecting the rest.
But now you're dealing with multiple moving parts:
You generally need service discovery, distributed tracing, and request routing between services. A function call that took 0.1ms is now a network hop that takes 5ms and can fail in 14 different ways.
The honest truth: microservices trade code complexity for operational complexity. If your team can't reliably operate Kubernetes, adding more services will only create more 3am pages.
Instead of managing servers, you write functions that run when something triggers them. The cloud provider handles the scaling. You only pay when those functions actually run.
Cold starts add latency — sometimes 500ms or more for JVM-based runtimes. Debugging across dozens of stateless functions gets messy. And the more you build around one cloud's runtime, the harder it gets to switch later.
The billing model flips at scale too. At low traffic, serverless is nearly free. At high traffic, a $50/month EC2 instance starts beating $500/month in Lambda invocations.
Most production systems don't use just one approach. There's usually a monolith at the core, and over time teams spin up a few services where they need independent scaling or faster deploys. Serverless shows up later for things like notifications, image processing, or cron jobs.
The typical evolution:
| Factor | Monolith | Microservices | Serverless | |--------|----------|--------------|------------| | Team size | < 20 engineers | 50+ engineers | Any | | Deploy speed | Minutes | Minutes per service | Seconds per function | | Debug difficulty | Low | High | Medium-High | | Infra overhead | Low | Very High | Low | | Vendor lock-in | Low | Medium | High | | Cold start pain | None | None | Real | | Best for | Most startups | Large orgs | Event-driven glue |
The architecture that works best is the one your team can operate. Not the one on the conference stage. Not the one Netflix uses.
If you have 5 engineers and you're spending more time on infra than features — you picked the wrong architecture, regardless of which one it is.
The best architecture is the one that matches your team's operational maturity, not their ambitions.
— blanho
Netflix ripped out Kafka, Cassandra, and three cache layers. Because every cache is a lie.
Synchronous calls work until they don't. Then you need a message queue. Here's why.
The hidden state in your servers is why you can't just 'add more boxes'.
# What you thought you needed:
services:
- user-service
- auth-service
- order-service
- payment-service
- notification-service
- api-gateway
# What you actually need to operate them:
infrastructure:
- service-discovery
- distributed-tracing
- circuit-breakers
- container-orchestration
- centralized-logging
- secret-management# Looks beautiful in the demo
def handler(event, context):
user_id = event["pathParameters"]["id"]
user = table.get_item(Key={"id": user_id})
return {"statusCode": 200, "body": json.dumps(user)}