The Teams Quietly Going Back to Modular Monoliths in 2026
Translated from the original Korean post. 한국어 원문 보기 →
The shift nobody talks about
There's a quiet trend running through engineering teams lately. Systems that got carved up into microservices are being put back together. Not the spaghetti monolith of old — something called a modular monolith.
A few years back my team did exactly what everyone else was doing: split the backend into microservices. It felt like the obvious call at the time. Independent deploys, clear team ownership, and that seductive word, "scalability." Every conference talk said so. Every job posting said so.
A few months in, reality looked different. Random 502s between services. Chasing a single bug meant five dashboards open and a debugging session with three teams in the room. Latency was unpredictable. Logs were scattered everywhere, and the one line I actually needed was always somewhere else.
Look at any individual service and it was fine. Look at the system as a whole and it was strangely fragile. That's when we started asking the question most teams hate saying out loud: did we overreact?

What microservices were supposed to fix
The appeal is real. In theory you get independent scaling, faster deploy cycles, clean ownership boundaries between teams, freedom in tech choices. All of that is genuinely achievable. Nobody's lying.
The problem is what actually happens at a small or mid-sized team. The moment something that used to be a function call becomes a network call, complexity jumps a level. Failure stops being deterministic and starts being probabilistic. A call that worked yesterday fails sometimes today, and chasing that "sometimes" becomes a job. Observability alone turns into somebody's full-time responsibility.
Spinning up a local dev environment gets painful. You've probably lived this: six services running locally just to load one screen. And latency accumulates quietly. Early on you don't feel it — with one or two calls, who'd notice. You only feel it once the system grows and the inter-service calls chain up.
Separate cause from effect and it reads like this. Microservices didn't create the problem. The cause was introducing distribution we didn't need, and everything above was the effect.
The modular monolith alternative
A modular monolith isn't a return to spaghetti code. You deploy as one unit, but enforce strict boundaries inside. Domains split cleanly, shared state between modules kept to a minimum.
Sketched simply:
[클라이언트]
↓
[API 레이어]
↓
[모듈형 모놀리스]
├── 인증 모듈
├── 주문 모듈
├── 결제 모듈
└── 알림 모듈
↓
[데이터베이스]
The key part is no network calls between modules. Just clean, controlled boundaries inside one codebase. You keep the boundary benefit microservices promised and drop the distributed-systems tax that came with it. Boundaries don't have to be made of network hops.
Where it was actually better
You feel the latency immediately
In the microservices version, every hop from order → payment → auth → database piles on network overhead (usually 5–20ms), serialization and deserialization, retry logic, and one more place to fail. Each looks small. Chained together, the weight adds up.
In the modular monolith it's 주문서비스.주문처리() → 결제서비스.청구() → 인증서비스.유효성검사() — plain function calls. Microseconds, not milliseconds. Same work, three orders of magnitude difference in units.
Debugging becomes humane again
When something breaks in microservices, you trace a request across service boundaries, stitch scattered logs together by hand, and pray distributed tracing was configured properly. The prayers get answered less often than you'd hope.
In a modular monolith it's one process, one log stream, one stack trace. Being able to actually follow the execution path end to end makes a bigger productivity difference than it sounds like. You catch bugs by tracing them, not by guessing.
Failure stays under control
In a distributed system one slow dependency sets off a chain reaction. Timeouts fire, retries kick in, the retries add load, and the whole thing goes down. Live through one retry storm and you'll know this isn't a joke.
Inside a monolith, failures are immediate and visible. No retry storms, and reasoning about how the system will behave gets much easier.
What the difference looked like in production
Take one simple flow we ran: create order → authenticate user → charge payment → persist order.
The microservices version involved four services and three network calls. Average latency around 180ms, spiking to 400–600ms intermittently. The failure modes were ugly too. Payment timeout → retry → double-charge exception. Auth lagging and blocking the whole request. It kept coming back just when we'd forgotten about it.
The modular monolith version handled the same business logic as function calls inside a single service. Average latency dropped to about 45ms, and p99 stayed stable. No internal retries needed, and failures showed up in predictable shapes — because where it breaks is right there in the code.
This wasn't just a few numbers getting better. It moved user experience and team productivity at the same time. Faster responses, and fewer 3am pages.
Actually making it modular
This is where a lot of teams slip. A monolith without boundaries is just a confusing pile of code. "We merged into a monolith" and "we designed it modular" are completely different statements. In a genuinely modular design, each module owns its interface, never reaches into another module's internals, and talks only through contracts.
Go 언어 예시:
// order/service.go
type OrderService struct {
paymentService PaymentService
authService AuthService
}
func (s *OrderService) CreateOrder(userID string, amount float64) error {
if err := s.authService.Validate(userID); err != nil {
return err
}
if err := s.paymentService.Charge(userID, amount); err != nil {
return err
}
return saveOrder(userID, amount)
}
Java 언어 예시:
// 결제 모듈
public class PaymentService {
public void charge(String userId, double amount) {
// 내부 로직
}
}
// 주문 모듈
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void createOrder(String userId, double amount) {
paymentService.charge(userId, amount);
}
}
What matters is that no database tables are shared between modules and nobody touches another module's internal classes directly. The second the payment module starts reading the order table, the boundary is already gone. That's the line between tidy code and modular design.

Common objections, and what's actually true
"It won't scale"
Half right. You usually start with vertical scaling, and that turns out to be enough more often than people expect. Bigger instances use memory more efficiently and cut network hops. Horizontal splitting can wait until you genuinely need it. Think about how many teams design for tens of thousands of RPS from day one and ever actually see that traffic.
"Teams will break each other's code"
Only happens when boundaries are weak. With solid module design and clear ownership, you get the same isolation microservices promised without the distributed complexity. Boundaries are held by agreement and code review, not by the network.
"You'll have to rewrite everything later"
Not necessarily. If modules are cleanly separated, pulling one out into its own service later takes minimal change. A modular monolith is a decent runway toward microservices — validate the boundaries in code first, then extract only the modules that really need to be distributed. Most teams do it in the opposite order.
Where microservices still win
None of this means microservices are useless. If parts of the system genuinely need to scale independently, distribution is still the better answer. Same if you're a large org with fully autonomous teams. Same for domains where isolation itself matters — payment systems, ML pipelines.
But teams that started with this architecture from day one? Most of them regret it eventually. That's what I've seen. Splitting because you needed to and splitting because it looked impressive show up differently once you're operating the thing.
Why teams are really going back
Not because microservices are bad. Because most systems never needed that much complexity in the first place, and debugging distributed systems costs too much. Latency often matters more than architectural elegance. And past a certain service count, developer productivity falls off a cliff. Those are the practical reasons teams are heading back to modular monoliths.
What teams are optimizing for is development speed, operational simplicity, and predictable performance. The modular monolith happens to sit close to that balance point. A stack trace you can follow alone at 3am is worth more than a beautiful distributed diagram — most people figure this out the hard way, once.
The honest downsides
A modular monolith isn't perfect either. A deploy affects the whole system. Keeping boundaries intact takes constant discipline. Your unit of scaling is coarser. And there's always the risk of drifting into a big ball of mud if you get lazy. Boundaries aren't something you draw once — they're closer to an agreement you defend in every code review.
Here's the difference that counts, though: these problems are far more tractable than distributed-system chaos. Code review and architecture guidelines are usually enough to hold the line. Certainly beats chasing problems hiding on the other side of a network.

It comes down to timing
The industry didn't regress. It re-timed itself against what it actually needs. Distributed systems are powerful. They also cost. The problem was getting the bill before we were ready to pay it.
What most teams figure out late is pretty simple. Building a scalable product doesn't require a distributed system first — it requires a system you can understand first. Complexity can wait until you actually need it.
The thing that matters isn't the architecture pattern itself. It's when you pick it, and why. Same pattern, different outcome, decided by those two.
Was this post helpful?
One click helps me write the next one