Hexagonal Architecture: 7 Choices for Maintainable Business Apps
Hexagonal architecture keeps business rules separate from the tools that carry requests and store data. It uses ports to describe what the application needs or offers. Adapters connect those ports to real tools, such as a web API, database, or payment provider. The aim is to protect useful business logic from frequent changes at the edges.
This guide explains the ports and adapters architecture through an invoice workflow. It covers boundaries, tests, data changes, costs, and a safe way to improve an existing system. You will also see when the extra structure helps and when a simple design is enough.
1. Understand what the boundary protects
Imagine a rule that decides whether an invoice can leave draft state. It may need a valid customer, at least one line, and an agreed currency. Those facts belong to the business. They should not depend on whether a request came from a web page, a batch job, or a partner API.
In a tightly mixed design, that rule might live inside a web controller. A batch job then copies it, while a second API adds a slightly different version. Over time, customers see different results from what should be the same action.
Hexagonal architecture gives that action a clear home. The application accepts a request to issue an invoice and applies the same rules each time. Each entry route handles its own transport details, then calls that action. As a result, a new route need not create a new version of the rule.
The original ports and adapters article by Alistair Cockburn describes this inside and outside distinction. The hexagon is a way to draw several connections. It does not require six ports, six layers, or six services.
2. Name ports after useful business needs
A port is a defined point of contact with the application. An incoming port may offer issue invoice or quote delivery. An outgoing port may request a tax rate or save an agreement. The name should make the need clear without naming a vendor.
For example, a payment port could express collect an agreed amount for an order. A provider adapter translates that request into the provider’s API. It then translates the result into terms the application understands, such as paid, declined, or outcome unknown.
However, do not make a port so vague that it hides real constraints. A general send anything method gives callers little guidance. Likewise, a port that copies every field from one vendor merely moves the vendor’s design behind another file name.
Write down what each result means. A timeout does not prove that a payment failed. The provider may have accepted it before the network broke. The application needs a path to check the outcome, rather than immediately charge again.
Use interfaces to express a promise
An interface is a code level description of that promise. It states what the caller supplies and what it can expect back. It should cover error cases as well as success. Business owners can help review the promise through examples, even if they never read the code.
The AWS guide to hexagonal architecture explains how adapters link these interfaces to outside tools. Its key idea is a dependency direction: the business core does not need to know each tool’s details. That direction matters more than the shape of the folders.
3. Give each adapter a narrow job
An adapter translates between two worlds. A web adapter checks request shape and maps an action into a response. A database adapter turns stored rows into application data. Neither should become a hidden second home for price, eligibility, or approval rules.
This does not mean all checks move into the core. The web edge must reject a malformed request and enforce its transport limits. At the data layer, keys and constraints still matter. The business layer, in turn, must validate the rules that hold across every entry route.
Keep vendor errors at the edge where possible. A provider’s rate limit code may map to a temporary failure in your application. Log enough detail for support without leaking private data. Then let the business flow choose whether to wait, retry, or seek review.
For teams comparing integration options, clear API documentation helps expose these edge cases early. Ask providers about duplicate requests, delayed outcomes, and status checks. A clean internal port cannot fix a missing external capability.
4. Keep transactions and side effects honest
A transaction groups database changes so they succeed or fail together. It does not usually include a remote email or payment service. If an invoice saves but its email fails, the application must know what to do next. A port alone does not make those actions atomic.
Choose the business rule for this split. The invoice may count as issued once its record commits, with email pending. Alternatively, the business may require another check first. Make those states visible, so staff do not mistake a delivery delay for a missing invoice.
An outbox is one option for reliable follow-up work. The application saves a record of the work alongside its own data in one transaction. A worker later sends it to the outside service. Still, the worker may send twice, so the receiver or adapter needs duplicate protection.
Also state which layer owns the transaction boundary. If several adapters each commit at different times, a single business action may leave partial changes. Review failure paths before choosing a shared transaction or a staged process with repair steps.
5. Test the core and the real adapters
The pattern makes it easier to test business rules without live infrastructure. A test adapter can return a known tax rate or simulate a declined payment. That gives the team fast feedback on decisions. It also helps reveal whether the core still depends on a particular database.
But a fake is only a model of an outside system. It may accept a date format that the real provider rejects. It may ignore a database constraint or a transaction limit. Therefore, keep real integration tests for each adapter as well.
Use contract tests to check shared request and response expectations. Then run a small set of tests against a real test environment. Check authentication, error mapping, timeouts, and duplicate calls. These checks serve different purposes and work best together.
A useful test suite should answer three questions. Does the business rule make the right decision? Can the adapter honor its port? Does the full path work with real settings? Passing only the first question can leave a polished core inside a broken system.
6. Trace an invoice through the design
Consider an illustrative wholesale portal. A user clicks issue invoice after reviewing a draft. The web adapter confirms the request format and the user’s identity. Next, the application checks the user’s right to act. It also checks that the draft meets the issue rules.
The core asks a customer port for the billing facts it needs. It checks required fields and applies the agreed currency rule. Next, a storage port saves the issued invoice and its follow-up work. The adapter handles the actual database commands and transaction mechanics.
After the commit, a worker attempts delivery. If the email provider times out, the invoice stays issued with delivery pending. The worker can retry safely using a stable message reference. Staff can see the pending state and correct an invalid address without issuing a second invoice.
Now imagine a partner wants to issue invoices through an API. The team adds a new entry adapter to the same action. It does not copy the issue rules into the partner route. However, it still needs to test partner identity, account access, and repeat requests.
Finally, suppose the firm replaces its email provider. The delivery port may stay the same if the business promise stays the same. Even so, the new adapter must prove its handling of errors and receipts. The pattern limits the change; it does not make the replacement free.
7. Adopt hexagonal architecture where change hurts
Start with a use case that mixes important rules with unstable tools. Payments, pricing, booking, and document delivery often reveal such pain. Map one action and its outside calls. Then move its rules behind a clear entry point while keeping current behavior under test.
Avoid a broad rewrite driven by folder names. First, put tests around the old behavior. Next, extract a port at a real change point and connect the existing tool through an adapter. Compare old and new results for the cases that staff care about.
During rollout, use useful operational signals to spot failed calls and stuck work. Track a business action across its adapters with a shared reference. A support team needs to locate one failed invoice, not decode a pile of unrelated logs.
Know when the extra structure costs more than it saves
A small tool that only edits a few records may not need many ports. Extra interfaces can make simple changes harder to follow. Use the pattern where rules have value of their own, several entry routes share behavior, or external tools change often.
Ask the team to explain each boundary in business terms. Which rule does it protect? How does it make a change safer? Which test becomes simpler? If the answer is only that the diagram looks clean, remove the layer or wait until a real need appears.
Plan for ownership too. Someone must keep port promises, adapter tests, and support notes current. A system with many abandoned interfaces can be harder to maintain than a small, direct one. Good architecture includes the team’s ability to run it.
Check the cost of a new provider before building the port
Take a likely provider change and walk through it on paper. Compare the old and new tools against the same business promise. One may return a final result at once. Another may accept a request and send the outcome later.
That difference may need a new application state, not just a new adapter. If the port promises an instant final answer, the second provider cannot honestly meet it. Revise the promise or choose a different tool. A boundary should expose a real limit rather than hide it.
Also compare data ownership. The new tool may keep its own customer reference or require a separate setup step. Decide where the mapping lives and how staff repair it. Do not scatter those references through the core merely to speed up the first integration.
Next, estimate the tests and support work for the switch. Include a case where both providers have work in flight during rollout. The team may need to route old records to the old adapter until they finish. Keep that route visible and temporary.
Finally, review whether the port makes the change easier to explain. If it still carries every old vendor field, simplify it around the business task. If it erases a detail the business needs, add that detail with a clear meaning. Good abstraction preserves the facts that matter.
Frequently Asked Questions
Is hexagonal architecture only for microservices?
No. A single application can use ports and adapters around key business actions. The pattern describes code dependencies and boundaries. It does not require separate services, cloud hosting, or a network between each part.
Will a database switch require no code changes?
No. The adapter, queries, migrations, and tests will likely change. Some database differences may also affect the application design. A good boundary reduces the spread of those changes, but it cannot erase real storage tradeoffs.
Do test adapters replace integration tests?
No. They help test rules quickly under known conditions. Real adapter tests must still check the database or provider behavior, credentials, and failure paths. Use both to cover the business promise and its actual delivery.
Protect the rules your business depends on
Choose one workflow where vendor changes or copied logic cause repeated trouble. Oxen Softwares can help define its ports, test its adapters, and improve the design in manageable steps.