Monolith First
Start with a monolith. Always.
This is not controversial advice among people who have actually built startups. It is only controversial among engineers who read about microservices architecture at big companies and want to apply it to their two-person team.
A startup with two engineers and microservices is cosplaying as Google. Google did not start with microservices either. They started with a monolith and decomposed it over years as they grew.
The monolith is your friend. It will carry you further than you think.
What a Monolith Actually Is
A monolith is a single deployable application. One codebase, one deploy process, one running process (or a few processes from the same codebase). All your business logic, all your API endpoints, all your background jobs — one repository, one deployment.
Monolith structure:
app/
models/
user.py
order.py
payment.py
views/
auth.py
checkout.py
dashboard.py
services/
email.py
billing.py
analytics.py
tasks/
send_receipts.py
sync_inventory.py
database: one PostgreSQL instance
deploy: one process, one server
Compare this to what a microservices version of the same app looks like:
Microservices structure:
user-service/ (own repo, own deploy, own database)
order-service/ (own repo, own deploy, own database)
payment-service/ (own repo, own deploy, own database)
email-service/ (own repo, own deploy, own database)
analytics-service/ (own repo, own deploy, own database)
api-gateway/ (routes requests to services)
message-queue/ (services communicate through this)
service-discovery/ (services find each other)
For a two-person team, the microservices version is at least five times more work to build, deploy, and operate. You now have five databases to manage, five deploy pipelines, five sets of logs to search, and a distributed system to debug.
Why Monoliths Win Early
Simplicity
A monolith is one thing to understand, one thing to deploy, one thing to monitor. When something breaks, you look in one place. When you need to change something, you change one codebase.
This simplicity compounds. Every feature is faster to build because you do not need to coordinate across services. Every bug is faster to fix because the entire system is in front of you. Every deploy is faster because there is one deploy.
Speed of Development
In a monolith, calling another module is a function call. In microservices, calling another service is an HTTP request (or gRPC call, or message queue publish). Function calls are instant, type-checked, and debuggable with a breakpoint. Network calls are slow, fragile, and require serialization.
Monolith:
order = Order.create(user=current_user, items=cart.items)
payment = Payment.charge(order)
Email.send_receipt(order, payment)
# Three function calls. Instant. Transactional.
Microservices:
order = requests.post("http://order-service/orders", json={...})
# What if order-service is down?
payment = requests.post("http://payment-service/charge", json={...})
# What if this fails after the order was created?
requests.post("http://email-service/send", json={...})
# What if this fails? Do we retry? When?
# Also: distributed transactions, eventual consistency, saga pattern...
The monolith version is three lines. The microservices version is three lines plus weeks of work handling failure modes.
Refactoring
In a monolith, renaming a function or changing a data model is a single commit. Your IDE can find all references. Your tests run against the whole system.
In microservices, changing a shared data model means coordinating releases across multiple services, maintaining backward compatibility, and hoping nothing breaks in between.
Early-stage startups change direction constantly. The monolith makes direction changes cheap. Microservices make them expensive.
Transactions
A monolith with one database gives you ACID transactions for free. "Create an order, charge the payment, and send a receipt" is one transaction. Either it all succeeds or it all rolls back.
In microservices, there is no transaction spanning multiple services. You need sagas, compensating transactions, or eventual consistency. These patterns are genuinely difficult to implement correctly. Many experienced engineers get them wrong.
You do not want to be debugging distributed transaction failures when you should be talking to customers.
The Scale Argument
"But we need to scale!" No, you do not. Not yet.
A well-optimized monolith on modern hardware handles an absurd amount of traffic:
Single-server monolith capacity (rough estimates):
- Rails app: 1,000-5,000 requests/second
- Django app: 2,000-10,000 requests/second
- Node.js app: 5,000-30,000 requests/second
- Go app: 50,000-100,000+ requests/second
For context:
- 5,000 req/s = 432 million requests/day
- Most startups never reach 100 req/s
Basecamp serves millions of users with a Rails monolith. Hacker News runs on a single process. Stack Overflow handles 1.3 billion page views per month with a monolith on a handful of servers.
If your startup reaches the point where a single server cannot handle your traffic, congratulations — you have a business problem that most startups would kill for. And at that point, you will have revenue and team to address it.
When to Extract Services
The monolith is not forever. At some point, you may need to extract services. The key word is "may" — many successful companies run monoliths far longer than you would expect.
Legitimate reasons to extract a service:
Team coordination cost. When multiple teams are frequently stepping on each other in the monolith, service boundaries can reduce coordination overhead. This typically happens around 15-30 engineers, not 2-5.
Genuinely different scaling needs. Your main app handles 100 requests per second, but your image processing pipeline needs to handle bursts of 10,000. Extract the image processing into a separate service that can scale independently.
Different deployment cadences. Your main app deploys 10 times a day, but your ML model retrains weekly. These naturally want to be separate.
Isolation requirements. Your payment processing has strict compliance requirements that the rest of your app does not. Isolating it in a separate service with its own security boundary makes compliance easier.
Extract a service when:
- You have evidence of a specific problem the monolith causes
- The benefit clearly exceeds the operational cost
- You have the team to operate it
Do not extract a service when:
- You read a blog post about microservices
- You think you might need to scale someday
- You want to use a different language for one component
- It seems like the "right" architecture
Real-World Monolith Success Stories
Shopify ran as a Rails monolith for years, handling billions of dollars in commerce. They eventually started extracting services, but only after reaching massive scale and having hundreds of engineers.
GitHub was a Rails monolith for over a decade. They only started decomposing when they had thousands of engineers and specific pain points that the monolith architecture could not address.
Instagram handled 30 million users with a Django monolith and a small team. When Facebook acquired them, the monolith was still working fine.
Etsy famously committed to their PHP monolith and invested in making it deployable 50 times a day rather than splitting it into services.
Stack Overflow runs one of the highest-traffic sites in the world on a monolith. They have written extensively about why this works and why microservices would be a worse choice for their team.
The Modular Monolith
If you are worried about your monolith becoming a tangled mess, the answer is not microservices. The answer is a modular monolith — a single deployment with clear internal boundaries.
Modular monolith structure:
app/
modules/
billing/
models.py
services.py
api.py
tests/
inventory/
models.py
services.py
api.py
tests/
shipping/
models.py
services.py
api.py
tests/
shared/
auth.py
database.py
Each module has its own models, services, and tests. Modules communicate through defined interfaces, not by reaching into each other's internals. The database is shared, but each module "owns" its tables.
This gives you 80% of the organizational benefit of microservices with none of the operational overhead. If you later need to extract a module into a service, the boundaries are already drawn.
Shopify pioneered this approach with their "components" architecture in Ruby on Rails. They enforced module boundaries with tooling rather than network calls. It works.
Common Pitfalls
Premature decomposition. Splitting your monolith into services before you have a reason to. Now you have all the complexity of distributed systems with none of the benefits.
"Clean architecture" as microservices. You can have clean module boundaries within a monolith. You do not need separate services to have separation of concerns. Use namespaces, modules, and clear interfaces.
Confusing monolith with mess. A monolith does not mean a big ball of mud. A well-structured monolith has clear module boundaries, consistent patterns, and organized code. The difference from microservices is that these boundaries are enforced by convention, not by network calls.
Microservices as a scaling strategy. The first scaling strategy for a monolith is a bigger server, then read replicas, then caching. You can go very far before you need service boundaries for scaling.
Splitting too early due to team growth. Three teams can work in one monolith with clear module ownership. You need a surprisingly large number of engineers before the coordination cost of a monolith exceeds the operational cost of microservices.
Key Takeaways
- Start with a monolith. This is not a compromise — it is the correct architecture for early-stage startups.
- A monolith gives you simplicity, speed of development, easy refactoring, and ACID transactions. These matter more than theoretical scalability.
- A single modern server handles far more traffic than most startups will ever see.
- Extract services only when you have evidence of specific problems: team coordination cost, genuinely different scaling needs, or isolation requirements.
- Many of the most successful companies in tech ran monoliths far longer than people assume. Shopify, GitHub, Instagram, Etsy, and Stack Overflow all prove that monoliths scale.
- The monolith is your friend until it is not. For most startups, that inflection point is years away — if it ever comes at all.