Hexagonal Architecture in TypeScript: Clean Architecture for Flexible Applications

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Hexagonal Architecture is Not Just a Buzzword, But a Lifesaver for Your Code?

Imagine you are building a house. You don't embed sockets into concrete walls, do you? You first run the wiring, mark the layout, and then carefully connect the sockets. In software development, it's exactly the same — your business logic (the application core) should not depend on how it is communicated with: REST API, database, or message queues.

Hexagonal Architecture (also known as "Ports and Adapters," or "Clean Architecture" in Alistair Cockburn's interpretation) solves precisely this problem. In this article, we will break down how to implement this architecture in TypeScript with real code examples. You will learn how to separate business logic from infrastructure, making your code testable and flexible.



What is Hexagonal Architecture in Simple Terms?

Imagine a hexagon. In the center is your application with business logic. On each face is a "port" (interface) through which the outside world can communicate with the core. Adapters are the concrete implementations: controllers, repositories, external service clients. They connect to the ports like a plug into a socket.

The main principle: dependencies point inward. External code (databases, frameworks) depends on the core, not the other way around. This allows you to change the database from PostgreSQL to MongoDB without touching a single line of business logic.



Key Concepts

  • Domain — business logic, entities, value objects, use cases. Has no external dependencies.
  • Port — an interface (in TypeScript, this is an abstract class or interface) that defines a contract for the outside world.
  • Adapter — a concrete implementation of a port. For example, a repository that works with PostgreSQL via TypeORM.
  • Application — the layer that coordinates the execution of use cases.


Practical Example: Order Management System

Let's design a simple order system. We will have Order and Product entities, a CreateOrder use case, a port for the repository, and a PostgreSQL adapter.



Step 1. Define the Core (Domain layer)

Let's create a domain/ folder. Entities and interfaces for ports live here. No framework dependencies!



// domain/order.entity.tsexport class Order {    constructor(        public readonly id: string,        public items: OrderItem[],        public status: 'pending' | 'confirmed' | 'shipped',        public total: number    ) {}

public addItem(productId: string, quantity: number, price: number): void { this.items.push({ productId, quantity, price }); this.total = this.calculateTotal(); }

private calculateTotal(): number { return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0); }}

export interface OrderItem { productId: string; quantity: number; price: number;}


Now let's define the port for the repository:



// domain/ports/order.repository.interface.tsimport { Order } from '../order.entity';

export interface IOrderRepository { save(order: Order): Promise<void>; findById(id: string): Promise<Order | null>; findAll(): Promise<Order[]>;}


Step 2. Use Case — The Heart of the Application

A Use Case is a usage scenario. It contains business logic and uses ports, but does not know who implements them.



// application/use-cases/create-order.use-case.tsimport { Order } from '../../domain/order.entity';import { IOrderRepository } from '../../domain/ports/order.repository.interface';

export class CreateOrderUseCase { constructor(private readonly orderRepository: IOrderRepository) {}

async execute(use

Blogs

Book Recommendations