Skip to main content
Learning Center
Python Programming

Architecture and State

Ports, Adapters, and Use Cases

Organize code around application decisions while HTTP, storage, and command-line details remain replaceable adapters.

Lesson 1 of 6 in the recommended order · About 25 min (estimate)

On this page

Outcome

Separate a use case from its delivery and persistence adapters.

Why it matters

At this level, code is judged by how safely it changes under load, failure, and team ownership. The technique in this lesson makes an important boundary visible enough to test and review.

Concept

A use case owns application policy. Ports describe what it needs; adapters translate HTTP, SQL, files, or queues into those ports. Dependencies point inward toward policy.

Read the code

from dataclasses import dataclass
from typing import Protocol

class Jobs(Protocol):
    def add(self, name: str) -> None: ...

@dataclass
class CreateJob:
    jobs: Jobs
    def execute(self, name: str) -> str:
        clean = name.strip()
        if not clean:
            raise ValueError("name required")
        self.jobs.add(clean)
        return clean

print("use case owns validation")

Read from the public behavior inward: identify the input boundary, the decision, and the observable result before studying syntax.

Inspect dependencies and an illustrative call

Depend on a port; call a supplied adapter

Listing versus illustration: The listing defines Jobs and CreateJob, then prints a message. It does not construct an adapter or call execute.

The call flow below adds an illustrative in-memory adapter and invokes execute(" review "). The dependency panels then describe the code and possible adapter roles separately.

Illustrative caller

Supply InMemoryJobs, construct CreateJob and call execute(" review ").

CallReturn: "review" from execute

CreateJob.execute

Strip the name, reject blank input, then call self.jobs.add(clean).

CallReturn: None from add

InMemoryJobs.add

Supplemental test adapter records "review" and returns None; not defined in the listing.

Observed in the supplemental invocation

Valid name
The adapter receives "review" once, then execute returns that cleaned name.
Blank name
ValueError("name required") is raised before any adapter call.

Application code dependency

CreateJob.jobs: Jobs

Type annotation refers to the Jobs port

Port requirement
add(name: str) → None
Policy owner
CreateJob.execute owns stripping and the blank-name rule.

CreateJob does not import a concrete SQL, HTTP or memory adapter. A Protocol annotation does not enforce conformance at runtime.

Storage adapters · conceptual

InMemoryJobs / SQLiteJobs

Provide the behavior required by Jobs

In-memory option
Keep accepted names in local state.
SQLite option
Translate add into a database operation.

Neither implementation appears in the listing. Structural conformance does not require inheriting from Jobs; storage/error guarantees must be specified and tested.

Delivery adapters · conceptual

HTTP route / CLI command

Invoke CreateJob.execute with an input name

Delivery role
Translate incoming input and outgoing result/error.
Shared rule
Both paths reach the same use-case validation.

This is runtime invocation direction. Code dependencies should point toward the application boundary, not make the use case import the delivery framework.

Failures and limits

Adapter raises
The error propagates; execute does not return clean.
Non-string input
The shown method assumes str; it does not validate types at runtime.
Not specified
Lookup, duplicate policy, rollback and durability are not defined by the add signature.

Do not infer transaction or exactly-once guarantees from this boundary alone.

Call/return arrows describe a supplemental invocation; labelled panel arrows describe type dependency or adapter roles. The Jobs protocol is a contract description, not an extra object called between the use case and adapter. All concrete adapters here are illustrations. The original listing only prints its message; these tests do not claim a live HTTP route or production database.

Predict the output

Predict the exact output before running the example.

Check your prediction

It prints use case owns validation. Nothing in the use case knows whether Jobs is backed by SQL, memory, or a remote service.

Modify the code

The listing defines CreateJob but does not call execute. Supply a small in-memory stand-in with an add(name) method, call execute with a valid name, then try a blank or whitespace-only name. Write down which layer rejects it and what the caller observes.

Review the change

Keep the failure at the narrowest boundary that owns the rule. Preserve a stable return value or exception contract so callers do not need to inspect implementation details.

Debug the bug

Putting domain validation only in an HTTP route lets a CLI or background worker bypass it. Policy belongs in the use case or model.

Try it yourself

Complete the focused implementation and run its deterministic checks.

Loading this exercise…

Practical challenge (optional)

Implement in-memory and SQLite adapters for the same port, then run one shared contract suite against both.

Sign in to track your progress on this exercise.

AI collaboration

Checkpoint

  1. Which layer owns the blank-name rule in the example, and why?
  2. In which direction should dependencies point between the use case and HTTP or SQL adapters?
  3. What shared test could prove two storage adapters honor the same port?
Answers
  1. The use case or domain model, because every delivery adapter must enforce the same application rule.
  2. Adapters depend on the application port; application policy must not import HTTP, SQL, or framework details.
  3. Run the same valid-name and adapter-failure checks through CreateJob with each adapter, and confirm blank names never reach add. The shown port declares only add(name), so lookup, duplicate handling and persistence guarantees need an explicit contract before a suite can assert them.

Sign in to track your progress on this exercise.

Summary and next step

You made the boundary explicit, predicted its behavior, tested a deterministic implementation, and examined its failure mode. Continue to the next lesson to combine this technique with a wider application or production constraint.

learning.goultergroup.com

The interactive parts of this page have not loaded. Reading and links still work; reload the page to try again.