Let's have an honest conversation nobody wants to have.
You chose microservices because that's what "serious" companies do. Netflix does it. Amazon does it. Your favorite tech influencer wrote a 40-tweet thread about it. So you split your app into 12 services, added Kubernetes, Redis, RabbitMQ, a service mesh, and now your team of 4 spends 60% of their time doing DevOps instead of shipping features.
At Gerus-lab, we've seen this pattern kill promising products. And we've also learned when microservices actually make sense — and when they're just expensive cosplay.
The Microservices Seduction
The pitch is irresistible: independent deployments, isolated failures, team autonomy, infinite scale. What's not to love?
The problem is you're not Netflix. Netflix has 2,000+ engineers and processes 15% of global internet traffic. You have a product idea, a runway of 18 months, and users who need features, not infrastructure poetry.
When we started building products at Gerus-lab, our team made the same mistake early on. One project — a GameFi platform — started as 8 microservices "for scalability." The result? We spent the first 3 sprints just making services talk to each other. The actual game logic? That came in sprint 6.
The Hidden Costs Nobody Talks About
Here's what the microservices evangelists won't tell you:
1. Distributed systems failure modes are brutal
A monolith fails in one place. A distributed system fails in a thousand subtle ways. Network partitions. Partial writes. Message queue backlogs. Deadlocks across service boundaries.
# What you think happens:
order_service.create() -> payment_service.charge() -> inventory_service.reduce()
# What actually happens at 3 AM:
order_service.create() # OK
payment_service.charge() # OK
inventory_service.reduce() # TIMEOUT
# Congratulations, you oversold inventory
# Now write a saga pattern to fix this
2. Latency compounds
Every inter-service HTTP call adds 2-10ms. String 6 services together and you've added 12-60ms to every request before you've written a single line of business logic.
3. Local development becomes a nightmare
Running 12 services locally requires Docker Compose files that look like infrastructure code. Your junior developer spends their first week just getting the environment running.
4. Observability costs multiply
Logging, tracing, and monitoring a microservices architecture requires a whole other stack — Jaeger, Zipkin, ELK, Prometheus, Grafana. That's 6 more things to break and maintain.
When We Stopped Fighting It
For our Web3 DeFi project (you can see similar work at gerus-lab.com), we made a deliberate choice: start as a well-structured monolith, extract services only when there's a proven need.
The results were immediate:
- Dev velocity: 3x faster feature delivery in the first quarter
- Debugging: "Something is broken" → fix time went from hours to minutes
- Onboarding: New developers productive in 2 days instead of 2 weeks
- Infrastructure cost: 70% cheaper (no Kubernetes cluster, no service mesh)
The monolith wasn't a compromise. It was the right engineering decision for that stage.
The Modular Monolith Pattern (What Actually Works)
Here's what we build now — and what we recommend to our clients:
src/
├── modules/
│ ├── auth/
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ ├── payments/
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ └── orders/
│ ├── domain/
│ ├── application/
│ └── infrastructure/
├── shared/
│ ├── events/
│ ├── database/
│ └── middleware/
└── main.ts
Modules communicate through a typed internal event bus — not HTTP calls, not message queues. No network hop. No serialization overhead. Full type safety.
// Clean module boundary — no direct imports between modules
@Injectable()
export class OrdersService {
constructor(private readonly eventBus: EventBus) {}
async createOrder(dto: CreateOrderDto) {
const order = await this.repo.save(dto);
// Other modules react to this event — no direct coupling
await this.eventBus.publish(new OrderCreatedEvent(order));
return order;
}
}
// PaymentsModule listens independently
@EventsHandler(OrderCreatedEvent)
export class OrderCreatedHandler {
async handle(event: OrderCreatedEvent) {
await this.paymentsService.initiatePayment(event.orderId);
}
}
This gives you:
- Module isolation — change payments module without touching orders
- Easy extraction — when (if) you need to split, the boundaries are already clean
- Zero network overhead — in-process communication
- Simple debugging — one process, one log stream
"But What About Scale?"
Fair question. The honest answer: you don't need microservices to scale, you need microservices to scale your teams.
Single-instance PostgreSQL handles 10,000 requests/second. A well-tuned Node.js app on a $200/month server handles more traffic than most startups will ever see. Horizontal scaling with a load balancer and read replicas takes you to millions of users.
We've seen a SaaS product at Gerus-lab handle 50K daily active users on a modular monolith running on 3 servers. Microservices never entered the picture.
Extract services when:
- A specific module has dramatically different scaling needs (e.g., video processing vs. API)
- You have 3+ full-time teams that can't coordinate on a single codebase
- A service needs a completely different tech stack for legitimate reasons (not hype)
Not because some blog post told you to.
The Checklist Before Splitting
Before you split that module into a service, ask:
- Does this module need to scale 10x independently from the rest?
- Do you have a dedicated team to own this service end-to-end?
- Have you profiled the current system and found this as the actual bottleneck?
- Do you have observability infrastructure to handle distributed tracing?
- Is your team productive enough that DevOps won't eat your roadmap?
If you answered "no" to 3 or more: keep the monolith.
What We've Learned From 14+ Products
After building platforms across Web3, GameFi, AI, and SaaS (you can explore our work at gerus-lab.com), here's the pattern that holds across every successful product we've shipped:
Start simple. Scale deliberately. Split surgically.
The most dangerous thing in software architecture isn't the monolith. It's choosing the wrong tool because you want to feel like you're building at Netflix scale, when you're still figuring out if your users want the product at all.
Microservices are a solution to an organizational problem. If you don't have that problem yet — don't create the solution.
Still convinced microservices are the answer? Drop your argument in the comments — I genuinely want to hear it. We've changed our minds before.
Need help architecting a backend that can actually ship? We've built 14+ products from zero to production, and we know when to keep it simple and when to distribute. Let's talk → gerus-lab.com
Top comments (0)