open atlas
↑ Back to track
Architecture Patterns ARCH · 04 · 02

Driving and driven sides

The hexagon has two sides: driving (left) where actors initiate — HTTP, CLI, tests — and driven (right) where the application reaches out — database, payment, email. Both sides communicate through ports owned by the domain. The domain sits at the center, isolated from both.

ARCH Middle ◷ 22 min
Level
FoundationsJuniorMiddleSenior

The B2B order platform had a working hexagonal structure for its HTTP channel — the controller called the confirmation port, the port was owned by the domain, the PostgreSQL adapter implemented the repository port on the other side. Then the team added Stripe for payment processing. The first implementation put the Stripe SDK call directly inside the order confirmation use case: await stripe.paymentIntents.create(...). Three months later, switching to a different payment processor for European customers required modifying the use case directly. The team then moved the Stripe call into a service class, but that service class imported stripe at the top level — so the domain module now had a transitive dependency on the Stripe npm package. A test that imported the domain to verify order confirmation logic would pull in Stripe, requiring a real API key or a manual mock. The team had correctly identified that Stripe was an external concern. But they had not correctly placed the boundary: the domain was still reaching out to infrastructure. The driven side — the right side of the hexagon — had not been inverted. Understanding which side the payment gateway belongs on, and why, is the structural lesson this unit teaches.

The two sides of the hexagon

Cockburn’s original description placed adapters on two distinct sides, and the asymmetry matters:

The driving side (left / primary) — actors that initiate interaction with the application. A driving adapter calls a primary port to make the application do something. The HTTP controller sends a POST and calls confirmOrder. The CLI sends a command and calls confirmOrder. An automated test calls confirmOrder. Each driving adapter translates its own input format into a port method call.

The driven side (right / secondary) — infrastructure that the application activates to do its work. A driven adapter implements a secondary port. The application calls IOrderRepository.save(order) — the Postgres adapter responds. The application calls IPaymentGateway.charge(amount) — the Stripe adapter responds. The application calls IEmailNotifier.send(message) — the SendGrid adapter responds.

The critical asymmetry: the driving side calls the application; the driven side is called by the application. Both sides are mediated by ports. But the direction of initiation is opposite.

The domain at the center

The domain sits at the center of the hexagon, surrounded on both sides by ports it owns. This is the structural consequence of applying the Dependency Inversion Principle (unit 02) at the architectural level: the high-level policy (domain) owns the abstractions; the low-level mechanisms (adapters) depend on those abstractions to plug in.

In the B2B platform, the domain defines:

// Primary ports (what the domain offers)
interface OrderConfirmationPort {
  confirmOrder(orderId: string, approverId: string): Promise<ConfirmationResult>;
}
interface InvoiceGenerationPort {
  generateInvoice(orderId: string): Promise<InvoiceId>;
}

// Secondary ports (what the domain requires)
interface IOrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}
interface IPaymentGateway {
  charge(customerId: string, amount: Money): Promise<ChargeResult>;
}
interface IEmailNotifier {
  send(recipient: Email, subject: string, body: string): Promise<void>;
}

All of these interfaces live in the domain package. None of them import HTTP, Stripe, Postgres, or SendGrid. The domain has zero knowledge of the outside world’s technology choices. It only knows what it needs (secondary ports) and what it provides (primary ports).

Mapping HTTP, CLI, and test-harness to the same primary port

One of the most valuable properties of the driving side is that it normalizes all entry points to the same port interface. The confirmation use case is defined once on the primary port. Any technology that can construct the required domain parameters can call it.

// HTTP driving adapter
class HttpOrderController {
  constructor(private port: OrderConfirmationPort) {}

  async post(req: Request): Promise<Response> {
    const result = await this.port.confirmOrder(
      req.params.orderId,
      req.body.approverId
    );
    return result.success
      ? Response.json({ status: 'confirmed' }, { status: 200 })
      : Response.json({ error: result.reason }, { status: 422 });
  }
}

// CLI driving adapter
class CliOrderConfirmCommand {
  constructor(private port: OrderConfirmationPort) {}

  async run(args: string[]): Promise<void> {
    const [orderId, approverId] = args;
    const result = await this.port.confirmOrder(orderId, approverId);
    console.log(result.success ? 'Confirmed' : `Failed: ${result.reason}`);
  }
}

// Test driving adapter — no framework needed, just the port
async function confirmOrderViaPort(
  port: OrderConfirmationPort,
  orderId: string,
  approverId: string
): Promise<ConfirmationResult> {
  return port.confirmOrder(orderId, approverId);
}

The confirmation logic executes identically regardless of whether the caller is an HTTP request, a CLI command, or a test. The test driving adapter is the simplest possible: just a direct call. No HTTP client, no port number, no server startup. This is available specifically because the primary port is owned by the domain and speaks domain language.

Why this works

Why does the driving side matter structurally, beyond just “multiple entry points”? Because the primary port is the formal definition of what the application can do — its public contract. Everything the application offers must be expressible through a primary port method. This has two consequences: (1) it prevents logic from living only in adapters (an HTTP-only feature that has no port method is architecturally invisible); (2) it makes the application’s capabilities enumerable — you can look at the primary port definitions and see every use case, without reading adapter code. This is why tests written against the primary port are the most meaningful regression tests — they test the application’s actual contract, not an adapter’s interpretation of it.

The driven side: secondary ports invert the infrastructure dependency

The driven side is where DIP from unit 02 becomes concrete. The domain defines what it needs — IPaymentGateway, IOrderRepository — in its own package, in domain language. The Stripe adapter implements IPaymentGateway; it depends on the domain’s interface definition. The Postgres adapter implements IOrderRepository; it depends on the domain’s interface definition.

The direction of source-code dependency on the driven side is the same as on the driving side: always inward, toward the domain.

// Secondary port — defined in domain package
// domain/ports/IPaymentGateway.ts
interface IPaymentGateway {
  charge(customerId: CustomerId, amount: Money): Promise<ChargeResult>;
  refund(chargeId: ChargeId, amount: Money): Promise<RefundResult>;
}

// Driven adapter — defined in infrastructure package
// infra/payment/StripePaymentAdapter.ts
import { IPaymentGateway, ChargeResult, RefundResult } from '../../domain/ports/IPaymentGateway';
import Stripe from 'stripe';

class StripePaymentAdapter implements IPaymentGateway {
  private client = new Stripe(process.env.STRIPE_KEY!);

  async charge(customerId: CustomerId, amount: Money): Promise<ChargeResult> {
    const intent = await this.client.paymentIntents.create({
      amount: amount.inCents(),
      currency: amount.currency,
      customer: customerId.value,
    });
    return ChargeResult.fromStripeIntent(intent);
  }

  async refund(chargeId: ChargeId, amount: Money): Promise<RefundResult> {
    const refund = await this.client.refunds.create({
      payment_intent: chargeId.value,
      amount: amount.inCents(),
    });
    return RefundResult.fromStripeRefund(refund);
  }
}

The StripePaymentAdapter imports from the domain (IPaymentGateway). The domain does not import from StripePaymentAdapter. The stripe npm package is a detail of the adapter — the domain has no transitive dependency on it. This is the fix for the opening story: moving the Stripe call behind a secondary port means the domain can be tested without Stripe, and switching payment processors means writing a new adapter without touching the domain.

lesson.inset.note

The terminology “driving/driven” maps directly to “primary/secondary” — driving adapters call primary ports, driven adapters implement secondary ports. Some sources use “left side/right side” because the conventional diagram places driving adapters on the left and driven adapters on the right. All three pairs (driving/driven, primary/secondary, left/right) describe the same structural distinction: which side initiates interaction, and which side responds. Use whatever terminology your team anchors on — the structural relationship is what matters.

Quiz

The order platform adds a Kafka consumer that reads OrderPlaced events from a topic and triggers invoice generation. Which side of the hexagon does the Kafka consumer sit on, and why?

Quiz

A developer argues: 'We should define IPaymentGateway in an infra/interfaces/ shared package so both the domain and the Stripe adapter can import it without either depending on the other.' Does this correctly apply DIP? Why or why not?

Quiz

The team writes an integration test that starts an actual HTTP server, sends a real HTTP request, and checks the HTTP response. It uses the real PostgreSQL database. Is this a test of the application through the primary port? What does it test that a domain-level port test cannot?

Recall before you leave
  1. 01
    What distinguishes a driving adapter from a driven adapter — and how does this relate to the direction of runtime calls versus the direction of source-code dependencies?
  2. 02
    Why is the test driver structurally a driving adapter, and what testing capability does this give you that HTTP-level tests cannot?
  3. 03
    In the B2B platform, the Stripe SDK was initially called directly inside the order confirmation use case. What structural rule did this violate, and how does moving it behind a secondary port fix it?
Recap

The hexagon has two sides, and the asymmetry is structural.

The driving side (left) contains adapters that initiate interaction with the application. Every entry point — HTTP controller, CLI runner, Kafka consumer, automated test — is a driving adapter. Each one translates its input format into a call on a primary port, which is owned by the domain. The application defines what it can do; driving adapters conform to that definition.

The driven side (right) contains adapters that respond when the application needs external capabilities. Every outbound dependency — PostgreSQL, Stripe, SendGrid, S3 — is behind a driven adapter. The domain defines what it needs through secondary ports it owns; driven adapters implement those ports and therefore depend inward toward the domain.

The domain sits at the center, surrounded by ports it owns on both sides. It has no knowledge of HTTP, databases, payment SDKs, or message queues. It only knows its own interfaces. This is DIP from unit 02 applied architecturally: the domain is the high-level policy that owns the abstractions; adapters are the low-level mechanisms that depend on those abstractions.

The test driver is a driving adapter — structurally identical to the HTTP controller. It calls the primary port directly, bypassing HTTP entirely. This is what makes fast, infrastructure-free domain tests possible: the domain is accessible through the same port interface that production adapters use, with fake driven adapters substituted for real infrastructure on the other side.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.