Blog

What I Learned Building a Gateway Using Hexagonal (Onion) Architecture

/5 min read/
ArchitectureIntegrationBackend

The architectural lessons that finally clicked while building a gateway service dealing with constant external change.

I have spent the past year working on a backend gateway service that sits between external platforms and internal domain systems. While I cannot share the business context, the architectural lessons are absolutely worth sharing.

I had read about hexagonal (or onion) architecture before, but it never fully clicked. It sounded reasonable in theory, yet vague in practice. That changed once I had to deal with constant external change, inconsistent integrations, and the kind of mess that slowly leaks across a codebase if you let it.

This post is me writing down what finally clicked, using diagrams to make the ideas concrete.

Scope (and what this does not cover)

  • How hexagonal architecture works in practice

  • Where responsibilities sit in a gateway service

  • How this approach affects testing and change

Out of scope: framework-specific implementations, deep domain modelling theory, and business logic or event schemas.

The mental model that finally stuck

Before getting into details, it helps to anchor on the big picture.

Diagram 1: Hexagonal (Onion) Architecture Overview: The system is drawn as concentric circles with the domain at the center and adapters at the edges.

The key rule is simple: all dependencies point inwards. The domain has no knowledge of HTTP, messaging, databases, or external APIs. Everything outside adapts itself to the domain, not the other way around. Once I internalised this rule, many design decisions became much easier.

Why this matters so much for gateway services

Gateway services are a stress test for architecture. They deal with external APIs you do not control, inconsistent payloads, retries, partial failures, and mismatched concepts. If your boundaries are vague, the mess spreads everywhere.

Diagram 2: Traditional Layered vs Hexagonal: In a traditional layered setup, dependencies flow outward instead of inward.

In the hexagonal version, HTTP, messaging, databases, and external APIs are just adapters. They do not talk to each other and they do not own business logic. The result is a much smaller change radius when something external changes.

How a request actually flows through the system

Architecture only becomes useful once you can trace a real request.

Diagram 3: Gateway Request Flow: An adapter receives a request, validates the structure, and maps it into a domain command. The use case orchestrates the workflow and communicates with ports implemented by infrastructure adapters.

On the way back out, adapters translate the result into whatever the external system expects. The use case never knows who called it or how the response will be sent.

The inversion that makes everything work

One of the most important lessons for me was understanding who owns interfaces.

Diagram 4: Port Ownership: Interfaces live with the domain or application layer, not infrastructure. The domain defines what it needs; infrastructure implements those interfaces.

This inversion of control keeps the domain stable and infrastructure replaceable. Swapping a database or external provider becomes an implementation detail, not a refactor. My test strategy improved immediately once this was in place.

Where logic actually belongs

Diagram 5: Where Logic Lives: Adapters handle parsing, serialisation, protocols, and mapping. Use cases orchestrate workflows and enforce application-level rules. The domain owns business rules and invariants.

Any time business logic leaks into adapters, it becomes harder to test, harder to reuse, and harder to reason about. I now treat test pain as an architectural smell.

Things that surprised me along the way

Mapping turned out to be real work, not boilerplate. Converting between external and internal models is where assumptions surface. Hexagonal architecture does not hide this work; it forces you to do it explicitly.

I also learned that not all validation belongs at the edges. Structural validation belongs in adapters, but business validation belongs in the core. If something would be invalid no matter how it arrived, it should live in the domain.

When I would (and would not) use this

  • Gateway services

  • Integration-heavy systems

  • Systems expected to change over time

I would think twice for small, short-lived services or simple CRUD APIs with minimal business rules. This is not about dogma. It is about choosing structure that matches the problem.

Key takeaways

  • Protect the domain from external concerns

  • Let the domain own its interfaces

  • Treat adapters as translation layers only

  • Mapping is a feature, not boilerplate

  • Test pain usually signals architectural drift

Final thoughts

Hexagonal architecture stopped being academic for me once I saw it absorb real-world mess without collapsing. If you are dealing with integrations, change, or long-lived systems, the upfront discipline is worth it. Start by protecting your core logic from the outside world. Once that foundation is solid, everything else becomes easier to reason about.