Software architecture · part 1 of 1
Hexagonal architecture: isolating the domain from the outside chaos
A complete lesson on ports and adapters, from the restaurant analogy to testing with fakes, with examples in Go, Java, Kotlin and TypeScript and animated diagrams.
intermediatePublished on · 27 min read
Imagine a restaurant. The customer places an order through the waiter, the tablet on the table or the delivery app, and the kitchen does not need to know which of the three it came from. It receives the order in the agreed format, prepares the dish and hands it back. If the restaurant replaces the tablet with a voice assistant tomorrow, the kitchen stays exactly the same. Hexagonal architecture, also called Ports and Adapters, applies this idea to software. The business rule sits in the center, without knowing whether the order arrived through HTTP, a message queue or an automated test. Everything that is technology (database, payment API, email sending, system clock) lives at the edge and talks to the center through contracts.
Two terms show up right away and keep coming back. A port is an interface that the core defines to say what it needs from, or offers to, the outside world. An adapter is the class that translates a concrete technology into that interface, such as a JDBC repository or an HTTP client. The full version of each term appears in sections 2 and 3.
A note on names: the code and the diagrams in this lesson use Portuguese identifiers, as in the original version. Pedido means Order, PedidoRepositorio means OrderRepository, RegistrarPedido means PlaceOrder and Salvar means Save. The code was kept as it was run, so that the quoted outputs stay real.
This lesson starts from the problem the architecture solves, goes through ports and adapters, separates who starts the conversation from who is called, shows how the direction of dependencies turns into packages and code (with examples in Go, Java, Kotlin and TypeScript), explains how to test with fakes, compares the architecture with layers, Clean and Onion, and ends with the most common pitfalls and the cases where it is overkill.
lesson map
- The problem: what happens when the business rule depends on the framework.
- Anatomy: core, ports and adapters.
- Who calls and who is called: driving and driven adapters.
- The direction of dependencies and the inversion that makes it possible.
- Package structure and the rule that no arrow goes from the center to the edge.
- Code examples, with the composition root that wires everything.
- Testing with fakes, compared with integration tests.
- Technical errors at the edge: who translates what.
- Comparison with layers, Clean Architecture and Onion Architecture.
- Common mistakes and good practices.
- When not to use it.
- Exercises and glossary.
1. the problem: the business rule stuck to the framework
Imagine an order service whose most important rule is this: an order can only be approved if it is pending. That rule does not depend on HTTP, Spring or PostgreSQL. It should remain true if the same code ran inside a terminal. In practice, the rule tends to end up in the wrong places. The Pedido class gains a persistence annotation, then an annotation to be injected by the framework, and at some point a method makes an HTTP call “just to validate something”. Each of these lines looks harmless on its own. Together, they mean that testing the rule requires starting a database.
The cost shows up at three moments. The first is testing. If the rule can only be verified with the whole environment up, the feedback loop goes from milliseconds to minutes, and the developer starts avoiding the test. The second is change. A framework version upgrade, which should be an edge matter, ends up touching the core, because the core imports framework classes. The third is reading. Someone arriving at the project and looking at the imports cannot tell what is rule and what is technical detail, because everything is mixed in the same package.
Alistair Cockburn described the architecture in 2005, in the article he called “Hexagonal Architecture”, with the alternative name Ports and Adapters. The central idea is a direction rule: the core does not know the edge, and the edge knows the core. The name comes from the drawing, a hexagon, but the number of sides carries no meaning. What matters is that the core has several faces, and each face receives a port, either incoming or outgoing.
2. anatomy: core, ports and adapters
The core is made of two things. The first is the domain, which holds the entities, values and rules that define the business, like the Pedido that only accepts valid states. The second is the use cases, which coordinate the domain to carry out an action from the point of view of whoever uses the system, such as “place an order”. The use case does not know whether the order goes to a database or to a spreadsheet. It knows that it needs something that saves orders and something that charges the payment, and it expresses that need as an interface.
Those interfaces are the ports. A port is defined by the core, and that matters more than it seems. When the use case needs to save an order, the PedidoRepositorio interface is born from what the use case needs, with the domain’s methods and types. It is not born from what the database offers. That is why it has methods like Salvar(pedido) and not ExecutarInsertSQL(tabela, colunas). The port speaks the language of the business.
An adapter is the class that implements the port with a real technology. The PedidoJdbcRepositorio implements PedidoRepositorio using SQL, the ClienteHttpPagamento implements GatewayPagamento using an external API, and the HTTP controller uses the input port to call the use case. The adapter translates in both directions. It receives JSON and turns it into a domain command, or receives a domain object and turns it into a database row.
The figure shows the whole system in a single image. The three controllers on the left are adapters that call the core, and the purple boxes are the ports that receive them. In the middle is the core, with the use case at the center and the domain attached to it. On the right are the adapters that the core calls, and at the end are the real technologies, PostgreSQL and the payment provider. Note that the call arrows (the dashed ones that move) cross the figure from left to right, but the code dependencies, which you will see in section 4, point in another direction. The colors follow a fixed map: orange for whoever calls, purple for the ports, blue for the output adapters and dark green for the database.
3. who calls and who is called
An adapter can sit on two sides of the core, and mixing up the two sides is one of the most common doubts. Cockburn called the adapters on the side that starts the conversation primary, and part of the literature uses the name driving. The ones the core calls to obtain something are secondary, or driven. A driving adapter is the one that pushes work into the system: HTTP controller, command line, queue consumer, scheduled job and even the test. A driven adapter is the one the system looks for to obtain something: database, payment gateway, clock, identifier generator and email service.
The separation matters because it defines which ports exist. The input ports are the operations the world can ask of the core, such as RegistrarPedido.executar(...). The output ports are the dependencies the core needs, such as PedidoRepositorio.salvar(...). Whoever writes the test of a use case is also a driving adapter. It calls the input port, which means the test needs nothing special inside the core.
The figure below shows a single request crossing the layers. It is also the figure that explains the five steps of an order.
Read the figure from left to right. The controller receives the POST /pedidos and calls the input port, which is step 1. The port forwards to the use case, which does the business work and calls the repository’s output port, steps 2 and 3. The JDBC adapter implements that port and talks to PostgreSQL, steps 4 and 5. The light green packets, which appear in the animation going the opposite way, represent the response coming back along the same path. In a still image that return is hard to tell apart, so it is worth running the figure to see both directions. The point to keep is that the use case never talks to PostgreSQL directly. It talks to a port, and whoever decides that the port is a database is the composition, which you will see in section 6.
4. the direction of dependencies
This is the part the whole architecture depends on, and for that reason it deserves care. There are two different arrows in a system. The first is the execution arrow: who calls whom at runtime. That arrow always goes from the controller to the database, because the request needs to get there. The second is the dependency arrow: who needs to know whose name in the source code, that is, who imports what. In a traditional layered architecture, the two arrows point in the same direction. The use case imports the concrete database class and calls it, and so the core depends on the infrastructure.
Hexagonal architecture separates these two arrows using the Dependency Inversion Principle. The port is defined in the core. The use case depends on the port, and the adapter also depends on the port, because it needs to implement it. This way, the execution arrow keeps going toward the database, but the code dependency arrow points inward, toward the core.
The figure compares the two worlds. At the top, the use case imports JdbcTemplate directly, and a Spring detail enters the core. At the bottom, the use case knows only the PedidoRepositorio interface, which lives in the core, and the JDBC repository points to it with a dashed arrow, which means implementation. Note that at the bottom the call still goes from the use case to the repository, so there is one execution arrow (calls) and one implementation arrow (implements), and both converge on the port. If you swap the database tomorrow, the bottom arrow stays the same, and only the blue rectangle changes.
In code, the inversion has a small and easy-to-recognize shape. The interface lives in the use case’s package, and the adapter imports that interface, never the other way around:
// application/registrar_pedido.go: the core defines the port
type PedidoRepositorio interface {
Salvar(p Pedido) error
}
In Go, this shape appears naturally, because interfaces are implicit. A type satisfies an interface just by having the right methods, so the RepositorioSQL adapter does not need to write implements anywhere. In Java and Kotlin the declaration is explicit, and in TypeScript it is structural, as in Go. Section 6 shows the four languages.
5. package structure
The dependency rule only holds if the folder structure makes the direction visible. A Go project can organize the pieces like this:
cmd/api/main.go composition root
internal/domain/ entities and pure rules
internal/application/ use cases and output ports
internal/adapter/in/http/ controller, driving adapter
internal/adapter/out/postgres/ repository, driven adapter
The folder names are not a detail. When someone opens the project, the tree already tells the story: what is in domain depends on nothing outside it, what is in application depends only on the domain, and what is in adapter depends on the core. The output port lives in application because the use case is the one that needs it. Some people prefer to put the ports in a separate port folder. That works, as long as the rule still holds: the port cannot import an adapter.
This direction can and should be verified by a machine. The figure below shows what the build needs to block: dependencies that go from the center to the edge and dependencies that cross the edge from one side to the other.
The green arrows represent allowed dependencies. The use case uses the domain, the controller uses the use case, and the repository implements the port. The pink arrows with the blocking symbol are the forbidden ones, and they are marked in two common situations. The first is the domain importing the framework, which would leave the core dependent on Spring. The second is an adapter calling another adapter, which creates hidden coupling between edge and edge. In an automatic check, these arrows become build errors. The Kotlin lesson on hexagonal architecture with ArchUnit shows how ArchUnit turns these rules into tests that fail when someone breaks them.
The next figure shows the same idea in a different format, as concentric rings. It is the visual version of what the name onion suggests: the domain sits at the heart, the application wraps the domain, and the adapters sit on the shell. Each ring only sees the rings further in.
Read the figure from the inside out. The domain, which contains Pedido and Centavos, has no arrow leaving it. The application has the use case, which uses the domain, and the ports. The adapters, on the shell, call the application and implement the ports. The color of the use case and the domain is the same (brown) because both belong to the core, and the color of the ports is purple because they are the boundary. If any arrow left the domain toward an adapter, the figure would stop being an onion and become a tangle, which is exactly what the rule wants to prevent.
6. code examples
The examples below were run in this revision, in the versions listed at the end of the lesson. The Go example is the most complete, because it shows the use case, the port, an output adapter that translates errors and a fake for testing. It is a single file so that you can run it with go run.
package main
import (
"errors"
"fmt"
)
// Core: no database, HTTP or framework imports.
type Pedido struct {
ID string
Total int64
}
var ErrPersistencia = errors.New("falha de persistência")
// Output port: defined by the core, implemented at the edge.
type PedidoRepositorio interface {
Salvar(p Pedido) error
}
type RegistrarPedido struct {
repo PedidoRepositorio
}
func NovoRegistrarPedido(repo PedidoRepositorio) *RegistrarPedido {
return &RegistrarPedido{repo: repo}
}
func (uc *RegistrarPedido) Executar(p Pedido) error {
if p.Total < 0 {
return errors.New("total negativo")
}
if err := uc.repo.Salvar(p); err != nil {
return fmt.Errorf("registrar pedido: %w", err)
}
return nil
}
// Output adapter: translates a technical error into a core error.
type RepositorioSQL struct{ fonte func(Pedido) error }
func (r RepositorioSQL) Salvar(p Pedido) error {
if err := r.fonte(p); err != nil {
return fmt.Errorf("%w: %v", ErrPersistencia, err)
}
return nil
}
// Fake for unit tests.
type RepositorioEmMemoria struct{ itens []Pedido }
func (r *RepositorioEmMemoria) Salvar(p Pedido) error {
r.itens = append(r.itens, p)
return nil
}
func main() {
fake := &RepositorioEmMemoria{}
uc := NovoRegistrarPedido(fake)
fmt.Println("feliz:", uc.Executar(Pedido{ID: "a", Total: 100}), len(fake.itens))
fmt.Println("negativo:", uc.Executar(Pedido{ID: "b", Total: -1}))
falho := NovoRegistrarPedido(RepositorioSQL{fonte: func(Pedido) error { return errors.New("conexão recusada") }})
err := falho.Executar(Pedido{ID: "c", Total: 5})
fmt.Println("erro:", err, "| é ErrPersistencia:", errors.Is(err, ErrPersistencia))
}
The real output, with Go 1.27.2, was this (the labels are in Portuguese because the code was kept as it was run: “feliz” is happy path, “negativo” is negative, “erro” is error):
feliz: <nil> 1
negativo: total negativo
erro: registrar pedido: falha de persistência: conexão recusada | é ErrPersistencia: true
Note three details. The use case imports nothing besides errors and fmt, and that is exactly the rule from section 5. The RepositorioSQL is the edge piece and only it knows what a connection failure is. And the last line shows that the error reaches the use case wrapped in ErrPersistencia, so whoever calls can check the category with errors.Is without knowing the driver.
In Java, the same idea uses a nested interface and a record for the order. The example below was also compiled with JDK 21.0.8 and produced the output salvos: 1 (“salvos” means saved):
import java.util.ArrayList;
import java.util.List;
public class Main {
record Pedido(String id, long totalCentavos) {}
// Output port, defined by the core
interface PedidoRepositorio {
void salvar(Pedido pedido);
}
// Use case: knows only the port
static final class RegistrarPedido {
private final PedidoRepositorio repositorio;
RegistrarPedido(PedidoRepositorio repositorio) {
this.repositorio = repositorio;
}
void executar(Pedido pedido) {
if (pedido.totalCentavos() < 0) {
throw new IllegalArgumentException("total negativo");
}
repositorio.salvar(pedido);
}
}
// Fake for unit tests
static final class RepositorioEmMemoria implements PedidoRepositorio {
final List<Pedido> itens = new ArrayList<>();
@Override
public void salvar(Pedido pedido) {
itens.add(pedido);
}
}
public static void main(String[] args) {
var fake = new RepositorioEmMemoria();
new RegistrarPedido(fake).executar(new Pedido("a", 100));
System.out.println("salvos: " + fake.itens.size());
}
}
In TypeScript, the port becomes an interface and the implementation uses implements. Since Node 24 strips types without transforming the code, the class declares the field explicitly instead of using the constructor shorthand. The snippet ran with the output salvos: 1:
type Pedido = { id: string; totalCentavos: number };
// Output port: the core defines the contract
interface PedidoRepositorio {
salvar(pedido: Pedido): Promise<void>;
}
class RegistrarPedido {
private readonly repositorio: PedidoRepositorio;
constructor(repositorio: PedidoRepositorio) {
this.repositorio = repositorio;
}
async executar(pedido: Pedido): Promise<void> {
if (pedido.totalCentavos < 0) throw new Error("total negativo");
await this.repositorio.salvar(pedido);
}
}
// Fake for testing
class RepositorioEmMemoria implements PedidoRepositorio {
readonly itens: Pedido[] = [];
async salvar(pedido: Pedido): Promise<void> {
this.itens.push(pedido);
}
}
const fake = new RepositorioEmMemoria();
await new RegistrarPedido(fake).executar({ id: "a", totalCentavos: 100 });
console.log("salvos:", fake.itens.length);
The Kotlin example follows the same shape, with an interface for the port and a constructor that receives the port. It was not compiled in this revision, because kotlinc is not on the PATH of the machine used. The version compiled with Kotlin 2.3.21 is in the ArchUnit lesson, mentioned in section 5.
interface PedidoRepositorio {
fun salvar(pedido: Pedido)
}
class RegistrarPedido(private val repositorio: PedidoRepositorio) {
fun executar(pedido: Pedido) {
require(pedido.totalCentavos >= 0) { "total negativo" }
repositorio.salvar(pedido)
}
}
All these examples have one piece that has not appeared yet: whoever creates the RepositorioSQL and hands it to the use case. That piece is the composition root. It is the only place that knows all the implementations and wires them together, usually at application startup. In Go, it is main.go. In Spring, it is a @Configuration class with @Bean methods. The figure below shows its role.
The composition root appears on the left, in its own group, because it has a different function from the other pieces. It creates the JDBC adapter, creates the use case and creates the controller, in that dependency order. The dashed arrows leaving the core toward the port show that the use case only receives the port and never the implementation. The root is the only piece that may break the direction rule, because it needs to see everything to assemble the graph. That is why it sits outside the package rule, as a declared exception. Whoever tries to put this assembly inside an adapter will find that the adapter starts to know the other adapters, and the rule breaks.
7. testing with fakes
The main practical advantage of the architecture shows up at test time. Since the use case depends on a port, the test can swap the real implementation for a fake, which is a simple, working implementation kept in memory. The Go example in section 6 already shows this: the RepositorioEmMemoria accepts orders and stores them in a slice, with no network or database. The test of the “negative total” rule runs in microseconds, and the happy path test confirms that the order reached the repository.
A fake is not the same as a mock. A fake has real behavior, only simplified, and the test checks the final state. A mock is configured to answer in a certain way and verifies that it was called as expected. Both are useful, but the fake tends to produce less fragile tests, because they survive a change in how the use case calls the repository, as long as the outcome stays the same. A mock that expects exactly one call to Salvar breaks at the first refactoring, even if the behavior stayed correct.
The figure below shows both kinds of test in the same architecture, and the difference between them is what sits on the other side of the port.
The top group is the unit test. It uses the real use case, which is production code, and swaps the port for an in-memory fake, marked in purple because it is still the port the core sees. The bottom group is the integration test. The use case leaves the scene and the test talks to the real JDBC adapter, which in turn talks to an ephemeral PostgreSQL, probably started by Testcontainers. Both tests are useful and answer different questions. The first answers whether the business rule is right. The second answers whether the translation to SQL is right. The lesson on Testing with Kotest, MockK and Testcontainers shows how to write the second kind in Kotlin.
There is a consequence that tends to be surprising. When the architecture is done well, most rule tests need no Spring, no database and no network, and that reduces the time of the whole suite. When it is not done well, the unit test of a simple rule needs to start the framework context, and the developer learns not to write rule tests. The architecture does not guarantee good tests, but it removes the obstacle that prevents writing them.
8. technical errors at the edge
The question that most reveals the quality of a boundary is: what happens when the database goes down? In an architecture without ports, the SQLException goes up the stack and reaches the controller, which now needs to know what SQL is to respond with the right message. The business rule, in turn, ends up deciding based on driver error codes. In hexagonal architecture, the translation happens at the edge. The adapter catches the technical error and returns an error expressed in the language of the core.
The figure follows the error path from back to front. PostgreSQL refuses the connection, and the database box turns red with the failure animation. The SQL arrow that should have gone through was blocked, which is represented by the blocking symbol. The JDBC adapter catches the technical exception and throws ErroPersistencia, which is a core category and not a driver class. From there, the red arrow continues through the port and the use case, always carrying the core’s error. The controller receives an error it knows how to interpret and responds 503, without leaking the stack trace to the client. Whoever is farther from the database never sees the word SQL.
This is the same mechanism the Go output showed in section 6, where errors.Is(err, ErrPersistencia) was true even though the original message contained “conexão recusada” (connection refused). The technical message remains available for the log, but the business decision uses the category. This has a cost: the adapter needs to know which driver errors are persistence errors and which are programming errors, and that classification must be done carefully. An SQL syntax error is a bug, and should not become an outage for the user. The core receives the category, and the edge decides the category.
9. comparison with other architectures
Layered architecture is the most common alternative and the one many teams already know. It organizes the system as a stack: user interface, business logic and data access. The top layer calls the one below, and the dependencies go down to the database. The problem is not the organization itself, which is useful for understanding a system, but the direction of the dependencies. Since the business logic directly calls the data layer, any change in the database may require changes in the logic, and testing the logic usually requires the database or a heavy mock.
The table summarizes the difference. It compares the two models on points that come up day to day, and does not try to be a list of virtues.
| Question | Layered | Hexagonal |
|---|---|---|
| Who depends on whom | The logic depends on the data layer | The logic defines the port, and the adapter depends on it |
| Where the database sits | Below the logic, as a foundation | At the edge, as a replaceable detail |
| How the rule is tested | With the database, or with mocks that imitate it | With a fake of the port, without infrastructure |
| Initial cost | Low | Higher, because of the ports and the composition root |
| When it pays off | Small systems and direct CRUD | Business rules that change more than the technology |
Clean Architecture is Robert C. Martin’s proposal, presented on his blog in 2012, which gathers similar ideas with its own vocabulary. It has circles with entities, use cases, interface adapters and frameworks, and it has an explicit dependency rule: code dependencies can only point inward. In practice, Clean Architecture and hexagonal teach the same rule with different names and drawings. Whoever has read one recognizes the other in a few minutes.
Onion Architecture is Jeffrey Palermo’s proposal, from 2008, and it also uses concentric circles with the domain at the center. The difference is more of emphasis than of content. The ArchUnit guide treats the two as the same pattern and offers a single API, onionArchitecture(), to verify both, as the lesson on hexagonal architecture with ArchUnit shows. So when someone says “onion” in a conversation, it is worth asking whether the person means the same thing as hexagonal or something different.
Two naming details help read the literature without confusion. The first is that Clean Architecture applied to Go has its own lesson, which starts from the same dependency rules. The second is that the word “port” refers to the interface, not to the HTTP route. A REST endpoint is not a port. The port is the contract the use case offers, and the endpoint is an adapter that exposes it.
10. common mistakes and good practices
The first mistake is letting a framework type escape through the port. A port that returns *sql.Rows, an entity annotated with @Entity or a Spring ResponseEntity turns the core into a client of the technology, even if the interface looks clean. The good practice is for the port to use only domain types and primitive types. When the adapter needs something extra, like a database-specific cursor pagination, it translates that detail before reaching the port.
The second mistake is creating one port per table, with generic CRUD methods. The result is a repository that looks like a table in the form of a class, and the use case ends up orchestrating queries instead of expressing the business. The good practice is for the port to have the verbs of the use case, like Salvar, BuscarPendentesDoCliente or MarcarComoPago, and not Insert, Update and Select.
The third mistake is the opposite: creating interfaces for everything, even without a second use. An interface with a single implementation and no fake brings no isolation benefit, only ceremony. This has a name, overengineering, and it is one of the most cited reasons for abandoning the architecture. The practical rule is to ask which dependency you want to swap or isolate in tests. If the answer is “none”, the port can wait.
The fourth mistake is putting business rules in the adapter. A controller that calculates a discount, or a repository that decides whether an order is late, spreads the rule across a place the core tests do not see. The good practice is for the adapter to only translate and route. Every decision you would like to test without starting infrastructure belongs to the core.
The fifth mistake is trusting the rule without verifying it. The direction of dependencies is easy to break with a distracted import, and a code review can let it through. The good practice is to turn the rule into an automated test, as the figure in section 5 suggests. Whoever does this should also plant a violation on purpose and confirm that the build fails before trusting the green. An architecture test that never failed proves nothing.
The sixth mistake is scattering the composition root. If each adapter creates its own dependencies with new, or if a global service locator is consulted from inside the core, the assembly becomes invisible and the core goes back to depending on everything. The good practice is to concentrate the assembly in a single place, like main.go in Go or a @Configuration class in Spring, and let the rest of the code receive dependencies through the constructor.
11. when not to use it
Hexagonal architecture has a cost, and that cost needs to be paid by some benefit. In a simple CRUD, where the screen reads and writes records with almost no rules, each port is one more layer of code to maintain, and the isolation benefit rarely shows up. The same goes for one-off scripts, prototypes that will be thrown away in a few weeks and services with a single screen and a single database that will never change.
The most useful criterion is to ask where the risk is. If the system has business rules that change often, many integrations and a team that needs to test those rules without infrastructure, the architecture pays for itself quickly. If the risk is in the technology, like a system that needs to scale a specific database or a data pipeline, organizing around ports may help less than a technology-oriented design. When there is no rule to protect, the best architecture is usually the simplest one that still keeps the code easy to read.
12. exercises
- Take a use case you have already written and list all of its dependencies. For each one, say whether it is driving or driven, and whether it is defined as a port or as a concrete class. If any concrete class appears in the core, describe the port you would create for it.
- In the Go example from section 6, add a new method
Buscar(id string) (Pedido, error)to the port and a matching fake. Then write a test that confirms the use case does not change when the fake swaps implementation. Explain why this test would not need to change if the port were implemented by a real database. - Create an
internal/adapter/out/httppackage with a client that importsRepositorioSQL. Run a dependency check (with ArchUnit, if you are in Java or Kotlin, or withgo list -depsin Go) and explain why the HTTP adapter should not depend on another adapter. - Write a negative test for the rule in section 5. Plant a violation, for example a framework import inside the domain, confirm the check fails with a clear message, and remove the violation. Note what the message says about the file path.
13. glossary
Hexagonal architecture: a style in which the business core does not depend on technical details, and the details depend on the core through ports and adapters. Also called Ports and Adapters.
Port: an interface defined by the core to describe what it needs from the outside world, or what it offers to it.
Adapter: a class that implements a port, or that calls a port, using a concrete technology such as JDBC, HTTP or a queue.
Driving adapter: an adapter that starts the conversation with the core, such as an HTTP controller, a command line or a test.
Driven adapter: an adapter that the core calls to obtain something, such as a database, a payment gateway or a clock.
Use case: a class or function that expresses a business action and coordinates the domain and the ports to carry it out.
Composition root: the part of the code that knows all the implementations and wires them together, usually at application startup.
Fake: a simplified, working implementation of a port, used in tests, such as a repository kept in memory.
Dependency rule: the principle that code dependencies point inward, toward the domain.
Dependency Inversion: the principle in which high-level and low-level modules depend on abstractions, and not on each other.
sources
Consulted on 2026-10-23:
- Alistair Cockburn, “The Hexagonal (Ports & Adapters) Architecture”, HaT Technical Report 2005.02, dated 2005-09-04: https://alistair.cockburn.us/hexagonal-architecture/. From this source came the name Ports and Adapters, the 2005 date and the correspondence between primary and driving, secondary and driven.
- ArchUnit user guide, section 8.1.2, on the onion architecture: https://www.archunit.org/userguide/html/000_Index.html. Consulted for the ArchUnit lesson on 2026-10-20, and not consulted again in this revision.
Run on 2026-10-23: the Go (go1.27.2), Java (JDK 21.0.8) and TypeScript (Node 24.21.0, with type stripping) examples ran, and the outputs quoted in the text are the real ones. The Kotlin example was not compiled in this revision.
Not verified in this revision: the dates for Robert C. Martin (2012) and Jeffrey Palermo (2008), which come from background knowledge of the field and not from a page consulted now. The characterization of Clean Architecture and Onion Architecture was also not checked against the original texts.