Short answers to what readers ask most about this topic.
01What is hexagonal architecture in simple terms?
Hexagonal architecture, also called ports and adapters, puts your business logic in a core that depends on nothing outside it. The core exposes ports, which are interfaces, and technologies such as HTTP, Postgres or a message queue connect through adapters. You can then swap or fake any adapter without touching the business rules.
02What is the difference between a port and an adapter?
A port is an interface owned by the application core that describes a purposeful conversation, such as saving an order. An adapter is the technology-specific code that implements or calls that port, such as a Postgres repository or an HTTP controller. One port can have several adapters, for example a real database and an in-memory fake.
03What are driving and driven ports?
Driving, or primary, ports are what the outside world calls to make the application do something, like a PlaceOrder use case called by a controller. Driven, or secondary, ports are what the application calls to reach something outside, like an OrderRepository. The difference is who starts the conversation.
04Is hexagonal architecture the same as clean architecture?
They share the central idea that source code dependencies point inward toward the domain, and Robert Martin lists hexagonal architecture as an influence on Clean Architecture. Clean Architecture prescribes named rings such as entities and use cases, while hexagonal architecture prescribes almost no structure beyond ports and adapters. In practice a project can satisfy both.
05When should I not use hexagonal architecture?
Skip it for simple CRUD, prototypes, short-lived scripts, or projects with one driver and one database that will not change. The cost is more files and indirection: in the example here one use case takes eight files instead of three. Introduce a port only for the dependency that hurts first, usually the database.
Hexagonal Architecture (Ports and Adapters) in TypeScript
What hexagonal architecture is, how ports and adapters keep business logic free of frameworks and databases, and a NestJS and Postgres example you can copy.
Hexagonal architecture, also called ports and adapters, keeps business logic in a core that depends on nothing outside it. The core defines ports, which are interfaces for what it offers and needs, and adapters such as an HTTP controller or a Postgres repository plug into them. Tests swap in an in-memory adapter, so rules run without a database.
A test for an order service that needs a running Postgres container, a migrated schema and a seed script just to check that two line items add up is testing the wrong thing. The arithmetic is one line of code, and the setup around it is the part that hurts. In ERP and POS work the logic worth protecting is exactly this kind of rule, and the database is the part that should be replaceable.
This post answers the question people search for: what is hexagonal architecture, and how do you apply it? It follows Alistair Cockburn's original 2005 article, then builds one small order service in TypeScript with a domain function, a repository port, a Postgres adapter and an in-memory adapter, wired in NestJS. The code type-checks under tsc strict and the tests ran; the output is shown.
What is hexagonal architecture and where did it come from?
Alistair Cockburn published it in 2005 as HaT Technical Report 2005.02, under the name Hexagonal Architecture, with Ports and Adapters as the alternative name. His stated intent is to allow an application to equally be driven by users, programs, automated tests or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.
The problem he was fixing is business logic leaking into the user interface. By his account that makes automated tests hard to write, makes it impractical to move from a human-driven system to a batch-run one, and makes it hard to let another program drive the application. The hexagon itself carries no meaning: he says the shape is not a hexagon because the number six is important, it simply leaves room to draw as many ports as the application needs.
What is the dependency rule: why does the domain depend on nothing?
Every source code dependency points inward. The domain depends on nothing. The application layer depends on the domain. Adapters depend on the application and the domain. The database driver, the web framework and the message broker sit at the outside, and nothing inside ever imports them.
This is what makes a port an interface owned by the core rather than by the technology. The core states what it needs in its own words, such as save an order and find an order by id. An adapter then does the translation. If the rule holds, you can delete the pg package from package.json and the domain and application folders still compile.
The most common way the rule breaks is a port that returns an ORM entity or a raw database row. The moment the interface mentions a table shape, the core depends on the database after all. Ports speak in domain types only.
What is the difference between driving and driven ports?
Cockburn splits the outside world into primary and secondary actors, which are also called driving and driven. The split is about who starts the conversation, and it decides which side of the hexagon a port lives on.
Driving (primary) port: the application offers it and the outside world calls it. In the example this is the PlaceOrder interface. Its adapters are the HTTP controller, a CLI command or a queue consumer.
Driven (secondary) port: the application needs it and calls out through it. In the example this is the OrderRepository interface. Its adapters are the Postgres repository and the in-memory fake.
The natural test substitute differs too. A driving adapter is replaced by a test that calls the port directly, and a driven adapter is replaced by a fake or mock.
Many teams only ever write driven ports because the repository is the obvious pain. Giving the use case its own driving interface is optional, but it is what lets a controller, a cron job and a webhook all call the same code without copying it.
How do you build it in TypeScript and NestJS?
Start with the folder tree. The boundary is a folder boundary: the domain and application folders import nothing from infrastructure, and only infrastructure imports NestJS or pg.
src/orders/
domain/
order.ts // Order, OrderLine, priceOrder() - pure rules
application/
order-repository.port.ts // driven port (interface)
place-order.service.ts // driving port (PlaceOrder) + its implementation
infrastructure/
orders.controller.ts // driving adapter: HTTP
postgres-order.repository.ts // driven adapter: Postgres
in-memory-order.repository.ts // driven adapter: tests
tokens.ts // DI tokens (Symbols)
orders.module.ts // the only file that knows which adapter runs
place-order.test.ts
The domain is a pure function with a business rule. The port is an interface in domain language. The use case is a plain class that receives the port through its constructor. None of the three files contains a decorator or a database import.
// domain/order.ts - no imports at all
export function priceOrder(lines: OrderLine[]): number {
if (lines.length === 0) throw new EmptyOrderError("An order needs at least one line");
return lines.reduce((sum, line) => {
if (!Number.isInteger(line.quantity) || line.quantity <= 0) {
throw new InvalidQuantityError("Bad quantity for " + line.sku);
}
return sum + line.quantity * line.unitPriceCents;
}, 0);
}
// application/order-repository.port.ts - driven port, domain language only
export interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
// application/place-order.service.ts - plain class, no decorators, no "pg"
export class PlaceOrderService implements PlaceOrder {
constructor(
private readonly orders: OrderRepository,
private readonly newId: () => string,
) {}
async execute(command: PlaceOrderCommand): Promise<Order> {
const order: Order = {
id: this.newId(),
customerId: command.customerId,
lines: command.lines,
totalCents: priceOrder(command.lines),
status: "placed",
};
await this.orders.save(order);
return order;
}
}
Adapters live in infrastructure. The in-memory repository clones on the way in and on the way out, because a real database never returns your own object reference and a fake that does can hide aliasing bugs. The Postgres adapter takes a minimal Queryable, which a pg Pool satisfies structurally, and maps snake_case columns back to the Order shape.
// infrastructure/in-memory-order.repository.ts
export class InMemoryOrderRepository implements OrderRepository {
private readonly rows = new Map<string, Order>();
async save(order: Order): Promise<void> {
// structuredClone: a real database never hands back your own object reference.
this.rows.set(order.id, structuredClone(order));
}
async findById(id: string): Promise<Order | null> {
const row = this.rows.get(id);
return row ? structuredClone(row) : null;
}
}
// infrastructure/postgres-order.repository.ts (save only)
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly db: Queryable) {} // a pg Pool satisfies Queryable
async save(order: Order): Promise<void> {
await this.db.query(
"INSERT INTO orders (id, customer_id, status, total_cents, lines) " +
"VALUES ($1, $2, $3, $4, $5) " +
"ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, " +
"total_cents = EXCLUDED.total_cents, lines = EXCLUDED.lines",
[order.id, order.customerId, order.status, order.totalCents, JSON.stringify(order.lines)],
);
}
// findById maps snake_case rows (customer_id, total_cents) back to the Order shape
}
The module is the single place that knows which adapter runs. NestJS custom providers use the provide key for a token and useClass or useFactory for the implementation. The NestJS docs show a Symbol token registered with provide and useClass, then injected with the Inject decorator. I used useFactory for the use case so that PlaceOrderService stays a plain class with no framework import, and the controller is the only file that carries Inject.
// infrastructure/orders.module.ts
@Module({
controllers: [OrdersController],
providers: [
{ provide: PG_POOL, useFactory: () => new Pool({ connectionString: process.env.DATABASE_URL }) },
// The one line that picks the adapter. A test module swaps it for useClass: InMemoryOrderRepository.
{ provide: ORDER_REPOSITORY, useFactory: (pool: Pool) => new PostgresOrderRepository(pool), inject: [PG_POOL] },
{
provide: PLACE_ORDER,
useFactory: (repo: OrderRepository) => new PlaceOrderService(repo, randomUUID),
inject: [ORDER_REPOSITORY],
},
],
})
export class OrdersModule {}
// infrastructure/orders.controller.ts - driving adapter
constructor(@Inject(PLACE_ORDER) private readonly placeOrder: PlaceOrder) {}
For the type-check I installed the NestJS and pg type packages in a scratch project and ran tsc with strict on. It passed with no errors. The Postgres adapter was only compiled against a stub of the Queryable shape, not run against a live database, so treat its SQL as a template and test it against your own schema.
Use Symbol tokens and keep them in the infrastructure folder. A string token invites typos that only fail at runtime, and a token exported from the application folder drags a framework concern into the core.
How does hexagonal architecture make testing easier?
The test builds the real service with the in-memory adapter. No container, no schema, no mocking library. The worked example is 2 items at 25000 plus 1 item at 15000: 2 x 25000 = 50000, plus 15000 gives 65000.
test("totals 2 x 25000 + 1 x 15000 = 65000 and persists the order", async () => {
const repo = new InMemoryOrderRepository();
let n = 0;
const service = new PlaceOrderService(repo, () => "ord-" + ++n);
const order = await service.execute({
customerId: "cust-7",
lines: [
{ sku: "WASH-PREMIUM", quantity: 2, unitPriceCents: 25000 },
{ sku: "WAX", quantity: 1, unitPriceCents: 15000 },
],
});
assert.equal(order.totalCents, 65000);
assert.deepEqual(await repo.findById("ord-1"), order);
});
// $ node --test dist/orders/place-order.test.js
// ✔ totals 2 x 25000 + 1 x 15000 = 65000 and persists the order (0.71ms)
// ✔ rejects an empty order and saves nothing (0.21ms)
// ℹ tests 2 ℹ pass 2 ℹ fail 0
Both tests passed in under a millisecond each when I ran them, but the point is not the speed of that run. The point is that the order rule is checked at the port, and the same test would run unchanged against any other OrderRepository. Keep a small number of integration tests that run the Postgres adapter against a real Postgres instance, since the fake cannot prove your SQL is correct.
How is it different from layered, onion and clean architecture?
They overlap more than they differ. Jeffrey Palermo's Onion Architecture (2008) and Robert Martin's Clean Architecture (2012) both put the domain at the centre with all coupling pointing inward, and Martin lists Cockburn's hexagonal architecture among the influences. What differs is mostly vocabulary and how much structure each prescribes.
Aspect
Classic layered
Onion / Clean
Hexagonal
Main picture
Stack of layers, top to bottom
Concentric rings around the domain
Inside, outside and ports on the boundary
Dependency direction
Each layer depends on the one below, so the UI reaches data access transitively
Inward only
Inward only, through ports
Where the database sits
At the bottom, a dependency of the business layer
In the outer ring as a detail
Behind a driven port, replaceable by an adapter
Prescribed structure
Layers such as presentation, business and data
Named rings, for example entities, use cases, interface adapters and frameworks
Almost none: ports and adapters as you need them
Main vocabulary
Layer, service, DAO
Entity, use case, gateway
Port, adapter, driving and driven
Palermo calls the coupling of the UI and business logic to data access the biggest offender in layered designs, because each layer depends on those beneath it and transitive dependencies are still dependencies. If you already apply the dependency rule, you are doing hexagonal architecture whatever you call it. Martin's Clean Architecture says the same, and the circles are schematic with no rule that you must have exactly four.
What does it cost, and when should you not bother?
The cost is indirection and files. In the example one use case needs eight source files: domain, port, service, controller, two repositories, tokens and module. A plain Nest controller, service and repository would be three. Each extra file is a place to navigate through when debugging, and a newcomer has to learn why the interface exists.
Skip it, or keep it minimal, when:
The feature is mostly CRUD with no business rule worth testing in isolation.
The code is a prototype or a script expected to live for weeks, not years.
There is one driver, one database and no realistic chance of either changing.
The team is one or two people who already find the codebase easy to navigate.
Reach for it when:
The same use case is called from more than one place, such as HTTP, a queue consumer and a CLI.
The rules are worth testing without infrastructure, as pricing, approvals or stock movement are in ERP and POS work.
An external system at the edge, such as a payment provider or a receipt printer, is likely to be swapped or to fail.
A reasonable middle path is to introduce a port only for the dependency that hurts first, usually the database, and add the rest when a second adapter becomes real. I would rather have one honest port than a port for every class.
Treat the domain as the thing you are building and everything else as a plug. Keep dependencies pointing inward, write ports in domain language, and give yourself an in-memory adapter so the rules can be tested in milliseconds. Then spend the extra files only where a second driver or a second implementation is a real possibility.