Hexagonal Architecture vs. Microservices
First published at Wednesday, 5 August 2026
Hexagonal Architecture vs. Microservices
This is a blog article out of a series where I reflect on what I learned during funding, growing (, and selling) SaaS product companies. While I write those things down for myself, since I believe self-reflection helps me learn, I also want to share my thoughts with anybody who might be interested. An overview of all related topics can be found below. I have a bunch of these blog posts lined up, so you also might want to follow me if you like this one.
There is a reflex I keep running into: a team identifies two bounded contexts in their domain, decides they should be separate, and concludes that separate means each one is its own service talking to the other over the network. The first two steps are right. The third does not follow, and it is expensive.
Bounded contexts deserve boundaries. I am not arguing against separation. I am arguing that separation is a property of your code, not of your network topology, and that the default place to draw the boundary is in-process, in a hexagonal or modular-monolith structure, not across a wire.
A boundary does not need a network
A hexagonal architecture already gives you a real, enforceable boundary between contexts: ports and adapters, a strict dependency direction, a domain core that does not know how it is called. My book lays out that layered structure in detail, so I will not repeat it here; it is deliberately light on the hexagonal-versus-distributed question, which is the gap this post fills.
The boundary is already there once the code is structured that way. Putting a network between the two contexts on top of that does not make the boundary stronger. It makes it slower, and it makes everything that crosses it harder to reason about. This is the same mistake I described for repositories: reaching for an expensive, hard-to-reverse structural split to express something the code already expressed for free.
In-process interfaces stay consistent. Network ones drift.
Here is the part engineers underweight. We assume the network is stable, because day to day it works. It is not stable. It fails, it delays, it delivers twice, it delivers out of order, and it does all of this at the worst possible time. Every call you move onto the network grows retries, timeouts, and partial-failure handling, and the interaction you could once read top to bottom now has to be reconstructed from logs and traces across two processes.
Keeping an interface consistent in code is trivial. Keeping it consistent across a network boundary is not, and there is a precise reason why.
When two components talk in-process, one compiler spans both sides of the interaction. The declared interface and the way it is actually called are checked against each other by the build. If they disagree, it does not compile. The boundary cannot silently drift, because the thing that defines it and the thing that uses it are compiled together.
Across the network, that single arbiter is gone. The two sides are authored by different people, in different files, in different repositories often, and compiled separately. Nothing forces the caller's idea of the contract and the callee's idea of it into agreement. They start aligned, and then they drift, quietly, until a payload that no longer matches anyone's assumptions takes something down in production. Moving the call onto the network cost you more than latency: it removed the only mechanism that was keeping the two ends honest.
There is real research on keeping an architecture model and the code consistent in-process [1] , precisely because the in-process boundary is the one a tool can span and check. What is conspicuous is that there is no equivalent for the network boundary, because nothing compiles both sides of it together. The in-process case is tractable; the cross-network case is left to discipline and hope.
The cargo cult of small services
Somewhere along the way people started repeating that a microservice should be small, a few hundred lines, sometimes less. Splitting by size is the worst possible rule, because the cost of a service boundary is the network between it and the next one. Optimising for tiny services optimises directly for the maximum number of network boundaries, which is to say the maximum amount of the drift and the debugging-across-processes tax I just described, in exchange for nothing. Size is not a reason to put a network somewhere.
When the network does earn its place
None of this is an argument that microservices are wrong. It is an argument against using them to model separation that the code should express. There are real reasons to pay for a network boundary: a component that has to scale independently of the rest, teams large enough that independent deployment is worth real coordination cost, genuinely different runtime or resource profiles, or a hard isolation requirement for security or tenancy. Those are runtime reasons, and the network buys you something concrete in exchange for its cost.
I wrote more about that side of it in when to use microservices. The rule that ties both posts together is simple: split onto the network when you have a runtime reason, never to express that two parts of your domain are conceptually distinct. For that, the code is the right place, and a hexagonal boundary is cheaper, faster, and far easier to keep honest.
Summary
Keep your bounded contexts separate, but draw the boundary in code, with a hexagonal or modular-monolith structure, not across the network by default. An in-process interface is checked by one compiler that will not let the two sides drift, and it lives in a single call stack you can actually debug. A network interface throws that arbiter away and hands you drift, partial failure, and cross-process debugging in return. Pay that price when a runtime reason makes it worth it, and not because you want to show that two things are separate.
References
- 1
- Konersmann, M. (2018). Explicitly Integrated Architecture - An Approach for Integrating Software Architecture Model Information with Program Code. PhD thesis, University of Duisburg-Essen. An approach to keeping an architecture model and the code consistent in-process; there is no comparable mechanism that spans a network boundary.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: