Go architectures · 3 MIN READ

Ports and adapters with a real request

Trace an HTTP adapter through an application operation to a storage port and compare test boundaries.

Ports and adapters is a way to keep a use case independent of delivery and infrastructure details. It is useful when one behavior has several entry points or storage implementations. The value comes from dependency direction, not from the number of folders named domain.

Hexagonal architecture · A MAP OF THE IDEA

  1. Driving adapter translates input
  2. Application use case owns its ports
  3. Driven adapter implements an external operation

Useful when

  • The same use case has CLI, HTTP or test drivers
  • Policy should be tested without a database

Watch out

  • Adapters still need real integration tests
  • A port per field creates unnecessary indirection

Implementation & operation cost: Medium

Fit depends on the task

Small tool or CLI3/5
Domain-rich application5/5
Independent distributed services4/5

Editorial suitability, not benchmarks or a universal ranking. Styles describe different axes and can be combined.

01

Trace one operation

A request body becomes a CreateOrder command. The application validates business rules and calls an OrderStore port. The HTTP adapter translates the outcome back into protocol terms. The storage adapter translates the port operation into its own database protocol. Domain rules do not inspect HTTP headers or SQL row objects.

02

Keep orchestration readable

This example is an application fragment. Validation runs before persistence. The error receives operation context while the caller retains access to its cause.

go
func (s *Service) Create(ctx context.Context, order Order) error {
    if err := order.Validate(); err != nil {
        return err
    }
    if err := s.store.Save(ctx, order); err != nil {
        return fmt.Errorf("save order: %w", err)
    }
    return nil
}
Read the code

The interface does not define transaction semantics by magic. If creating an order and publishing an event must be atomic, design that consistency boundary explicitly, for example with a database transaction and an outbox.

03

Test at more than one boundary

A fake store can test the use case cheaply, but cannot prove that a SQL adapter writes the right schema. Add integration tests for adapters and a smaller number of end-to-end journeys. Keep fakes faithful to important failure contracts, including cancellation and duplicate requests. Avoid adding layers when a small program has no substitution or ownership problem to solve.

04

Put the port beside the use case

The application asks for a capability such as reserving inventory, not for a generic database connection. Define the smallest interface that the application consumes, with domain values and context.Context for work that can block. A SQL adapter implements that contract; a test fake implements the same observable behavior. The executable constructs the adapter and injects it into the use case. The port does not import HTTP, SQL drivers or framework types. One struct can implement several consumer-specific interfaces without declaring it explicitly.

05

Map errors and transactions deliberately

A repository adapter can translate sql.ErrNoRows into the application absence contract instead of leaking SQL details. An HTTP adapter maps that stable error category to a response and logs once. If a use case changes several records atomically, expose one transaction-scoped operation or unit-of-work boundary whose lifetime the use case controls. Calling three repository methods independently does not create a transaction. Keep a remote payment request outside a long database transaction and design reconciliation for a payment that succeeds before local persistence fails.

06

Test the contract, not just the fake

A fake that always returns success can hide wrong SQL behavior. Run a shared repository contract against both fake and real adapter: missing record, duplicate identifier, cancellation and commit failure matter. Use application tests for business rules and adapter tests for SQL mapping. The extra abstraction earns its place when rules outlive a transport or infrastructure choice; a small read-only command can call one concrete implementation directly and introduce a port later.

TAKE THIS WITH YOU

Use a port to express an application need and an adapter to translate an external mechanism. Verify both the use case and the concrete boundary.

Check your understanding

Which statement is correct about a storage port?

Go to the source

Progress stays in this browser. No account needed.