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

Use cases and interactors

The Use Cases ring orchestrates entities to achieve one application goal. An interactor is the class that implements a use case: it receives a request model, calls entities, and returns a response model — never a framework type.

ARCH Middle ◷ 22 min
Level
FoundationsJuniorMiddleSenior

The B2B order platform’s backend had grown to over forty service methods. When a new engineer joined, their onboarding task was to find where invoice generation happened. They searched for generateInvoice and found it called from six places: the HTTP controller, a background job, a webhook handler, a batch reconciliation script, an admin CLI, and an email-processing service. Each callsite passed slightly different arguments. Two of them skipped the “billing hold” check. One used the database connection object directly. There was no single place in the codebase that said: “here is what it means to generate an invoice — the input it requires, the steps it takes, the output it produces.” The logic was smeared across caller code. When Clean Architecture describes the Use Cases ring, it is answering exactly this problem: there should be one class per application use case, and that class should encode everything the use case needs — input shape, entity orchestration, output shape. The interactor is that class.

The Use Cases ring and what it owns

In Clean Architecture’s ring model, the Use Cases ring sits directly outside the Entities ring. It contains application-specific business rules — rules that express what this particular application does with the entities.

The distinction matters. An entity contains enterprise-wide business rules: an Order must have at least one line item; an Invoice total must equal the sum of its line items. These rules would be true in any application built on this business. A use case contains application-specific rules: to generate an invoice, first check that the order is confirmed, then check for billing holds, then create the invoice entity, then publish an event. These steps are specific to this application’s process. Different applications built on the same domain (say, a mobile app vs an operations console) might have different use cases but share the same entities.

The Use Cases ring owns:

  • One class per use case (the interactor)
  • The input port interface (what the use case accepts)
  • The output port interface (what the use case produces)
  • The request model (the data structure crossing in)
  • The response model (the data structure crossing out)

The interactor pattern

An interactor is the class that implements a single use case. It contains the application-logic steps for that use case and nothing else. It does not know about HTTP, SQL, JSON, or any framework concept.

// All types here are in the Use Cases ring — no framework imports
interface GenerateInvoiceInputPort {
  execute(request: GenerateInvoiceRequest): Promise<void>;
}

interface GenerateInvoiceOutputPort {
  presentSuccess(response: GenerateInvoiceResponse): void;
  presentError(error: InvoiceGenerationError): void;
}

class GenerateInvoiceInteractor implements GenerateInvoiceInputPort {
  constructor(
    private readonly orders: IOrderRepository,
    private readonly invoices: IInvoiceRepository,
    private readonly billingHolds: IBillingHoldChecker,
    private readonly presenter: GenerateInvoiceOutputPort,
  ) {}

  async execute(request: GenerateInvoiceRequest): Promise<void> {
    const order = await this.orders.findById(request.orderId);
    if (!order) {
      this.presenter.presentError(new InvoiceGenerationError('ORDER_NOT_FOUND'));
      return;
    }
    if (order.status !== OrderStatus.Confirmed) {
      this.presenter.presentError(new InvoiceGenerationError('ORDER_NOT_CONFIRMED'));
      return;
    }
    const hold = await this.billingHolds.checkForHold(order.customerId);
    if (hold.active) {
      this.presenter.presentError(new InvoiceGenerationError('BILLING_HOLD_ACTIVE'));
      return;
    }
    const invoice = Invoice.createFor(order);
    await this.invoices.save(invoice);
    this.presenter.presentSuccess(new GenerateInvoiceResponse(invoice.id, invoice.total));
  }
}

GenerateInvoiceInteractor is the one canonical place that says: “here is what generating an invoice means.” Every caller — HTTP controller, batch job, CLI, webhook handler — calls this interactor. No caller can skip the billing hold check because it is inside the interactor, not in the caller.

Request models and response models — why they exist

A common early shortcut is to pass the HTTP request object directly into the use case:

// Wrong: Use Cases ring importing a framework type
async execute(req: ExpressRequest): Promise<ExpressResponse>

This creates a source-code dependency from the Use Cases ring to the HTTP framework — an outward-pointing dependency that violates the concentric rule. Worse, it makes the use case untestable without a real HTTP request object.

The fix is request models and response models: plain data structures defined in the Use Cases ring, carrying exactly the data the use case needs in domain terms.

// Correct: Use Cases ring defines its own data shapes
class GenerateInvoiceRequest {
  constructor(
    public readonly orderId: OrderId,
    public readonly requestedBy: UserId,
  ) {}
}

class GenerateInvoiceResponse {
  constructor(
    public readonly invoiceId: InvoiceId,
    public readonly total: Money,
    public readonly generatedAt: Date,
  ) {}
}

The HTTP controller (in Interface Adapters) parses the HTTP request and constructs a GenerateInvoiceRequest. The interactor never sees an ExpressRequest. The interactor produces a GenerateInvoiceResponse. The HTTP presenter (also in Interface Adapters) converts it to a JSON HTTP response. The translation between framework types and use-case types is the Interface Adapters ring’s entire job.

Why this works

Why go to the trouble of defining separate request/response models instead of passing a plain object or a generic dictionary? Because the request model documents exactly what a use case needs. It is the use case’s API contract — verifiable by type checking, discoverable by IDE navigation, explicit in code review. A plain object or generic dictionary hides this contract; any caller can pass any shape and only fails at runtime. The request model is documentation that the compiler enforces. This is especially important as the team grows: the interactor is the authoritative specification of what a use case requires. New callers read it, not the HTTP controller.

Input ports and output ports

The interactor communicates through two port interfaces, both defined in the Use Cases ring:

Input port (GenerateInvoiceInputPort): the interface the interactor implements. Callers (HTTP controllers, CLI adapters, batch jobs) import this interface and call execute. They never import the concrete interactor class. This means the entire interactor implementation can be swapped or mocked in tests without changing any caller.

Output port (GenerateInvoiceOutputPort): the interface the interactor calls to deliver results. The interactor does not return a value — it calls the output port, which the Interface Adapters ring implements (as a presenter). This is the dependency inversion applied to the response side: the interactor does not know about HTTP status codes, JSON serialization, or streaming — it calls a domain-language method like presentSuccess(response) and lets the presenter decide how to render it.

This two-port structure mirrors hexagonal architecture’s primary and secondary ports, now applied specifically at the Use Cases ring boundary. The interactor is the application “hexagon” for that one use case.

lesson.inset.note

Many implementations skip the output port and just return the response model from the execute method instead. This is simpler and still valid — it only gives up the ability to have the interactor drive multiple presentation concerns from one call. For most use cases, returning the response model directly is the pragmatic choice. The output port pattern is most useful when one use case must update multiple output channels simultaneously (update the UI, publish an event, write a log) without knowing what those channels are. Both approaches preserve the dependency rule; the choice is a tradeoff between simplicity and flexibility.

Quiz

The B2B platform's GenerateInvoiceInteractor currently accepts a GenerateInvoiceRequest and calls a GenerateInvoiceOutputPort presenter. The team wants to add a REST endpoint AND a GraphQL endpoint, both triggering the same invoice generation. What changes, and what stays the same?

Quiz

A developer argues: 'The output port pattern adds unnecessary indirection. Our interactor should just return a GenerateInvoiceResponse and let the controller serialize it.' Is this valid, and what does it give up?

Quiz

The team discovers that PlaceOrderInteractor and GenerateInvoiceInteractor both need to check for billing holds on a customer. They extract a CheckBillingHoldService with shared logic. In which ring does CheckBillingHoldService belong?

Recall before you leave
  1. 01
    What does an interactor own, and what is it forbidden from importing?
  2. 02
    What is the role of a request model, and why is it not the HTTP request object?
  3. 03
    What are input ports and output ports at the Use Cases ring boundary?
Recap

The Use Cases ring solves the “smeared logic” problem: when the same application operation is implemented differently across six callsites, there is no single place that encodes what the operation means. The interactor pattern addresses this directly — one class, one use case, one place to find the complete logic.

An interactor receives a request model (a plain data structure defined in the Use Cases ring, containing domain-typed fields and no framework types), orchestrates entities through interface-defined repositories and services (also defined in the Use Cases ring), and delivers results through an output port or a returned response model. Every caller — HTTP controller, CLI adapter, batch job, webhook handler — constructs a request model and calls the input port. They all go through the same logic, including all the same checks.

The two-port structure (input port and output port) applies the dependency inversion symmetrically: callers depend on the input port interface (inward dependency from Interface Adapters to Use Cases); the interactor calls the output port (the Interface Adapters ring implements the output port interface, which is defined in Use Cases). No inner-ring code names any outer-ring type.

The next lesson examines the Entity core — the innermost ring, which contains the most stable code — and the honest cost of this whole structure: the mapping tax, the ceremony, and the circumstances under which the overhead is not worth it.

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.