Low-level designDeep dive9 min
Ports and adapters
Keep the core of an application free of devices, databases and frameworks. The core declares ports, interfaces in its own terms, and adapters at the edge connect each port to a real technology, or to a fake one in a test.
Kiln bookings at a pottery studio
KilnDesk reads a typed command, refuses a full firing, writes the booking into the shared spreadsheet and texts the member.
1 · The situation
Kiln bookings at a pottery studio
A pottery studio fires its kiln once a day, and each firing has room for two members' work. Members book a shelf at the front desk, where whoever is on duty types a command such as book Mia Tue into a tablet. KilnDesk.handle() reads the command, refuses a full firing, writes the booking into the studio's shared spreadsheet, and texts the member.
The programs can't reach a real spreadsheet or send real texts, so StudioSheet keeps its rows in memory and prints each one it adds, and SmsSender prints the text it would send. In the first version the desk creates both itself:
class KilnDesk {
private static final int SHELVES = 2;
private final StudioSheet sheet = new StudioSheet();
private final SmsSender sms = new SmsSender();
String handle(String command) {
String[] parts = command.split(" ");
String member = parts[1];
String day = parts[2];
if (sheet.countOn(day) >= SHELVES) return "The " + day + " firing is full";
sheet.add(member, day);
sms.send(member, "your shelf in the " + day + " firing is booked");
return member + " booked for " + day;
}
}The class diagram of the first version, drawn from its code: the desk owns a spreadsheet and a text sender.
> book Mia Tue Sheet: Mia, Tue SMS to Mia: your shelf in the Tue firing is booked Mia booked for Tue > book Ravi Tue Sheet: Ravi, Tue SMS to Ravi: your shelf in the Tue firing is booked Ravi booked for Tue > book Jo Tue The Tue firing is full > book Jo Wed Sheet: Jo, Wed SMS to Jo: your shelf in the Wed firing is booked Jo booked for Wed
2 · The change
A rule you can only run against the real sheet
The studio wants to change the rule: members on the firing course get priority, and a cancellation should free the shelf. Before changing it, they want a test that proves today's behavior, such as "the third booking on a day is refused". That's where this design stops them:
Wanted | What stands in the way |
|---|---|
Test the rule on a laptop | The desk writes to the shared spreadsheet and sends real texts every time it books |
Start a test with a full Friday | The bookings live in whatever sheet the desk created |
Send emails instead of texts | Edit the desk, which holds the booking rule |
Take bookings from the studio's website too | The rule is reachable only through |
The rule is a few lines, and it's the part most likely to change. Yet it can't run without the spreadsheet and the text service, and the only way to reach it is to type a command, because the class that holds it also reads the input and chooses the services.
A first attempt adds interfaces, BookingLog and Notifier, and types the fields with them. It helps less than it looks:
class KilnDesk {
private static final int SHELVES = 2;
private final BookingLog log = new StudioSheet();
private final Notifier notifier = new SmsSender();
String handle(String command) {
String[] parts = command.split(" ");
String member = parts[1];
String day = parts[2];
if (log.countOn(day) >= SHELVES) return "The " + day + " firing is full";
log.add(member, day);
notifier.send(member, "your shelf in the " + day + " firing is booked");
return member + " booked for " + day;
}
}The fields have interface types, but the desk still creates the spreadsheet and the text sender, so a test still can't give it anything else, and the website still has to type commands. The practice checks flag the first part: KilnDesk builds its own StudioSheet and its own SmsSender.
3 · The fix
Let the core ask for its ports
Start from the interface version. The interfaces are right; what's wrong is who chooses the services, and who reads the input. Move both out of the core:
KilnDesk takes a BookingLog and a Notifier in its constructor, and its rule is a plain method, book(member, day). It names only the interfaces, which are written in its own terms: how many shelves a day has taken, and a message to a member.
class KilnDesk {
private static final int SHELVES = 2;
private final BookingLog log;
private final Notifier notifier;
KilnDesk(BookingLog log, Notifier notifier) {
this.log = log;
this.notifier = notifier;
}
String book(String member, String day) {
if (log.countOn(day) >= SHELVES) return "The " + day + " firing is full";
log.add(member, day);
notifier.send(member, "your shelf in the " + day + " firing is booked");
return member + " booked for " + day;
}
}The desk works as before, and the last line comes from a test that touches neither the sheet nor the text service:
> book Mia Tue Sheet: Mia, Tue SMS to Mia: your shelf in the Tue firing is booked Mia booked for Tue > book Ravi Tue Sheet: Ravi, Tue SMS to Ravi: your shelf in the Tue firing is booked Ravi booked for Tue > book Jo Tue The Tue firing is full > book Jo Wed Sheet: Jo, Wed SMS to Jo: your shelf in the Wed firing is booked Jo booked for Wed Test, a third booking on Fri: The Fri firing is full; texts sent: 2
The core depends only on its two ports. The driving adapter calls the core; real and test adapters implement the ports, and every arrow points inward.
4 · The idea
A core, its ports, and adapters at the edge
Ports and adapters, also called hexagonal architecture, divides an application into:
- the core: the application's rules, here
KilnDesk; - ports: interfaces through which the core talks to the outside, here
BookingLogandNotifier; - adapters: classes that connect the core to one technology, here the typed commands, the spreadsheet and the text service, or to a stand-in in a test.
Its aim is an application that can be driven by people, programs, tests or batch scripts alike, and developed and tested apart from the databases and services it will run with.
Side | Who starts the conversation | Examples |
|---|---|---|
Driving (primary) | The outside calls the core | A web controller, a command line, a scheduler, a test |
Driven (secondary) | The core calls the outside through a port | A database, a spreadsheet, an email or text service |
Here the driving side is DeskCommands, and the test is a second driving adapter; both call book(), the core's driving port. In a larger application that port is often an interface too, so the core can be swapped behind it. BookingLog and Notifier are driven ports. The rule that holds the shape together is about direction: every source dependency points inward. Adapters know the core; the core knows nothing about any adapter.
Requests come in through the driving adapter, and calls go out from the core to the services, but the driven adapters' arrows point back at the ports: the core depends on nothing outside itself.
5 · When breaking it is fine
Ports you don't need
Every port is an interface, an adapter and some wiring. That pays off where the outside is slow, expensive, nondeterministic or likely to change: hardware, networks, databases, clocks. It doesn't for:
- Libraries that are just code. A math or date library has nothing to swap and is quick to test; call it directly.
- Small tools and scripts. A script that reads one file and prints a total has no core worth isolating.
- One port per class. Ports describe what the core needs, such as "how full a day is", not every class it happens to call.
What the core talks to | A port? |
|---|---|
A database, a spreadsheet, an email or payment service | Yes: slow, external, or hard to run in a test |
The clock or a random number source | Often: tests need to control them |
A collection class or a string utility | No: it's fast, local and deterministic |
A framework's web request object | Kept in the driving adapter, translated before it reaches the core |
6 · Getting it right
Name ports in the core's language
- Shape ports around what the core needs.
countOn(day)asks how full a firing is;appendRow(sheetId, range, values)would be the spreadsheet's API leaking in. - Keep framework types out of the core. A port that takes an HTTP request or returns a database row ties the core to that technology again.
- Create adapters in one place.
main, the composition root, decides which adapters plug in; the core never does. - Test the adapters too. Fake adapters prove the rules; a few tests of the real adapters, against real hardware or storage, prove the connection.
Language | A port is usually |
|---|---|
Java | An interface in the core's package |
C++ | An abstract class with pure virtual functions, in the core's headers |
JavaScript | A documented shape the core expects, or a function passed in |
Python | A |
7 · Look-alikes
Hexagons, onions and clean circles
Several architectures share this idea under different names and drawings.
Idea | How it relates |
|---|---|
Layered architecture | The domain depends on data access below it; ports and adapters reverses that arrow |
Clean architecture | Concentric circles (entities, use cases, interface adapters, frameworks) with the same rule: source dependencies point inward |
Onion architecture | Another set of rings around the domain, with the same inward rule |
Dependency inversion | The principle each port applies: the core owns the abstraction, and details depend on it |
Adapter pattern | The class-level pattern each adapter is: converting one interface into the one a client expects |
The names differ mostly in how many rings they draw inside the core. All of them keep business rules away from frameworks and devices, and all of them get there by letting the core own its interfaces.
8 · In practice
What the checks look for
The practice checks flag a class that creates a concrete collaborator with new when an abstraction for it exists, reported as "KilnDesk builds its own StudioSheet". The fix is the one on this page: take the interface in the constructor and let the caller choose.
In the parking-lot problem, pricing is such a port: the lot asks for a fee and doesn't care how it's computed. Design the parking lot so the lot is given its pricing rather than building it.
Check yourself
4 questions
Takeaways
Remember this
- The core holds the rules and talks to the outside only through ports: interfaces in its own terms.
- Adapters connect the core to one technology, or to a fake in a test; every dependency points inward to the core.
- An interface alone isn't enough: if the core still creates its database or service classes, nothing else can plug in.
- Driving adapters, such as a command reader, a web form or a test, call into the core; driven adapters are called by it through a port.
- Use ports for slow, external or nondeterministic things, not for plain code libraries.