One bounded context per microservice is how you build a distributed monolith
DEV Community

One bounded context per microservice is how you build a distributed monolith

The rule that builds a distributed monolith

I'm sure you are familiar with the concept that one bounded context should map to one microservice. The rule is silently constructing the very thing that everyone says they despise. A monolith wearing a network for a costume. The rule sounds right until you count the hops.

Domain-Driven Design gave us bounded contexts. The definition of bounded context - separating a large domain into multiple sets of models, each with a specific definition and implication - was a blast of fresh air in a field with strong partiality to hardcode solutions at early design steps. After that, a decision was made that each context should have its own deployment, its own repository, and its own on-call schedule.

Dividing a domain into services does not eliminate the coupling between those parts; it simply shifts the coupling to the wire. A Java Code Geeks breakdown articulates this well: the simple one-context-per-service mapping results in a distributed monolith. It's the same hairball, but with even less-obvious failure modes.

The physics don't care about your architecture diagram

Here's the part nobody wants to put on the whiteboard. Function calls and network calls are not the same thing by a rounding error. Jeff Dean compiled this list of common operations and measured the typical and maximum latency for each:

  • An L2 cache hit takes about 3 to 7 nanoseconds
  • A network round trip inside the same datacenter takes 500,000 to 1,000,000+ nanoseconds

So when you take a fast in-process call and turn it into a synchronous HTTP or gRPC hop, you're paying a 10,000x to 100,000x latency tax. Per hop. ๐Ÿ˜ฌ

Your nice service boundary is the stuff of fairy tales. In the real world it's a series of coin flips, and each one has the potential to time out, decide to retry, or simply keel over. According to Sam Newman, the author of Building Microservices, a distributed monolith can be defined as a system that has "all the downsides of a distributed system and all the downsides of a monolith." So, you'll have to deal with pager duty as well as coupling. Well done!

Prime Video already ran the experiment for you

This is not a made-up scenario. The Amazon Prime Video team actually wrote a case study in 2023 about their Video Quality Analysis tool. The initial design was what you'd expect to see today: AWS Step Functions coordinating a fleet of Lambda microservices, with data being passed back and forth via S3 buckets. It crashed when the load reached just 5% of what was expected. Can you believe it? The orchestration overhead and the continuous network calls just overwhelmed the system.

What they did went against common beliefs in many Slack channels. They consolidated the microservices once again into a single process, but this time, the process was running on ECS, and they also shifted data transfer from S3 to in-memory. Outcome: infrastructure costs were reduced by 90%. The "monolith" won with a huge advantage.

The uncomfortable question underneath all of it

Kelsey Hightower, the former Google Principal Engineer and Kubernetes pioneer, hit the nail on the head with the teams "creating 50 deployables, but it's really a distributed monolith." It's fifty things to deploy, one thing to actually reason about.

Simon Brown, the C4 model guy, gave us the line that I think about whenever someone tries to grab for microservices too early: "If you can't build a well-structured monolith, what makes you think you can build a well-structured set of microservices?" That's the whole thing. Distribution doesn't fix bad boundaries. It ships them to more machines and adds latency for the trouble.

What I'd actually do

I'm not anti-microservice. I'm anti-splitting-for-the-sake-of-a-heuristic.

  • Draw your bounded contexts. Keep DDD.
  • Then make them modules in one deployable, not fifty services on day one.
  • Split a piece out only when it has a real, measured reason: independent scaling, a different runtime, a team boundary that actually hurts.
  • Treat every network hop as a cost you have to justify, not a default you reach for.

The bounded context simply defines the context within which a model is valid. It is a design boundary and wasn't intended to be a physical boundary in terms of how deployed. It's important to first build a well-structured monolith. If you are unable to do that, then no Kubernetes in the world can rescue you.

What is the tiniest, dumbest excuse for splitting a service into a microservice that should have just been a function call instead that you've ever seen a team use?

Top comments (0)

Comments

No comments yet. Start the discussion.