50%

الدرس 1 من 2

Ports, Adapters, and Which Side Drives

The pattern's original goal was an application that behaves identically whether a person, a test or a scheduled job is asking. That goal explains every part of its structure.

قراءة 11 دقيقة

Alistair Cockburn described hexagonal architecture in 2005 with a specific complaint in mind: business logic kept leaking into user interfaces, and applications could not be tested or automated without one. His fix was to insist that the application expose its capabilities through explicit ports, and that every external thing — the web interface, the test harness, the database, the payment provider — reach it only through an adapter.

The hexagon shape carries no meaning beyond "more than four sides, so stop thinking in layers". What does carry meaning is that the ports fall into two groups with completely different design rules — and this is the part that summaries usually flatten.

Driving and driven sides of one application

Left-hand actors drive the application. Right-hand systems are driven by it. The interfaces on the two sides are owned differently.

On the left, three driving adapters — a web controller, a scheduled job and an acceptance-test harness — all call the same driving port. In the centre, the application core with its use cases and domain. On the right, three driven adapters — a Postgres repository, an email sender and a payment provider — each implementing a driven port that the core declares.

Driving adapters

Web controller

Scheduled job

Test harness

Application core

Driving ports

the capabilities offered

Use cases and domain

no technology here

Driven ports

the capabilities needed

Driven adapters

Postgres repository

Email sender

Payment provider

QuestionDriving (primary) portDriven (secondary) port
Who defines itThe application, describing what it can doThe application, describing what it needs
Who implements itThe application itselfAn outside adapter
Who calls itAn outside adapter — controller, job, testThe application
Typical example`EnrolLearner.execute(input)``EnrolmentRepository.save(enrolment)`
What replacing the adapter buysA new way to reach the same behaviourA new technology behind the same behaviour
The asymmetry that matters: who defines the interface, and who depends on whom.
One application, three driving adapters, and a test that uses the same port the web does.typescript
// Driving port: what the application offers. Owned by the application.
export interface EnrolLearnerPort {
  execute(input: { learnerId: string; courseId: string }): Promise<EnrolOutput>;
}

// Driven port: what the application needs. Also owned by the application.
export interface EnrolmentRepository {
  activeEnrolmentCount(learnerId: LearnerId): Promise<number>;
  save(enrolment: Enrolment): Promise<void>;
}

// Driving adapter 1 — HTTP.
export async function POST(request: Request) {
  const output = await enrolLearner().execute(await request.json());
  return Response.json(output);
}

// Driving adapter 2 — a nightly job. Same port, no HTTP anywhere.
export async function backfillEnrolments(rows: readonly PendingRow[]) {
  for (const row of rows) await enrolLearner().execute(row);
}

// Driving adapter 3 — an acceptance test, with driven ports faked.
const app = new EnrolLearner(new InMemoryEnrolmentRepository(), fixedClock('2026-03-01'));
await app.execute({ learnerId: 'l-1', courseId: 'c-1' });

In practice

A port that earned its keep during an outage

A ticketing platform sent all transactional email through one provider, behind a driven port with three methods: send a templated message, check delivery status, and suppress an address. The provider had a six-hour regional outage on the morning of a major on-sale.

Constraints

  • Roughly 40,000 confirmation emails expected during the on-sale
  • A secondary provider account existed but had never been used
  • Templates lived in the primary provider's system
  • The team had 90 minutes before the on-sale opened

Decision

Write a second adapter against the existing port using the secondary provider, with templates rendered in the application rather than by the provider, and switch by configuration.

Why

The port described what the application needed, not what the provider offered, so the second adapter had only three methods to satisfy. Rendering templates in the application had been a deliberate earlier choice, made specifically so that provider-side templates would not become an invisible dependency — which is what made the 90-minute window feasible.

What it cost

The secondary provider had no delivery-status webhook, so the status method returned "unknown" and one internal dashboard degraded for a day. The team accepted a degraded dashboard over 40,000 undelivered confirmations, and afterwards recorded that the port's status method was the weakest part of the abstraction because it assumed a capability not every provider has — a small Liskov problem they chose to live with rather than remove.

Asked how many ports the system should have

As a developer

Counts external systems and creates one port each, since that is the pattern applied consistently and each port makes its dependency mockable in tests.

As an architect

Counts the capabilities the application needs, which is usually a smaller number than the systems it talks to and occasionally a larger one. Three services might sit behind one `Notifications` port if the application only ever asks "tell this person this thing", while a single database might sit behind three ports if three modules need genuinely different things from it. The port boundary follows the application's vocabulary, not the vendor list.

Your team wraps the database, the cache, the logger, the clock, the file store and the feature-flag service each in its own port, then finds that most tests use the real implementations anyway because the fakes are hard to keep faithful. What went wrong?Reveal

Ports were created by counting external systems rather than by naming capabilities the application genuinely needs to vary or fake. Two symptoms follow. Fakes that must reproduce complicated behaviour — a cache with eviction, a file store with partial writes — drift from the real thing, so tests pass against a fiction; that is why teams quietly go back to the real implementation. And ports for things with one obvious implementation, such as a logger, buy nothing at all. The productive move is to keep ports where a second implementation exists or where the fake is trivially faithful — a clock, a repository, an email sender — and call the rest directly, exactly as the dependency-inversion lesson in Chapter 2 argued.

Key takeaways

  • A port is an application-owned interface; an adapter is technology-specific code that plugs into one.
  • Driving ports are implemented by the application and called from outside; driven ports are the reverse.
  • The pattern is working when a full business scenario can be driven with every driven port faked.
  • A driven port that mirrors the vendor SDK is a wrapper, not an abstraction.
  • Count capabilities, not external systems, when deciding how many ports to have.