Hexagonal Architecture in Kotlin: When Code Breathes Freely
\n\nImagine you are building a house. You could immediately pour the foundation, erect the walls, and install the windows. Or you could first design the framework, lay the utilities, and only then—finish the interior. In the programming world, the second approach is called hexagonal architecture (also known as Ports & Adapters). And Kotlin, with its conciseness and powerful type system, is the ideal language to bring this concept to life.
\n\nThis article is not a boring textbook. It is a practical guide that will show you how Hexagonal Architecture in Kotlin turns your code into a flexible, testable system independent of external services. We will break down an example from scratch, write real code, and see how it works in practice.
\n\nWhat is Hexagonal Architecture and Why Do You Need It?
\n\nAlistair Cockburn, who coined the term, compared an application to a hexagon. In the center is your business logic. On the edges are ports (interfaces) through which it communicates with the outside world. Adapters are implementations of these interfaces for specific technologies: a database, a web framework, a message queue.
\n\nThe key idea: business logic knows nothing about how data is stored or which framework is used. It simply calls port methods. Want to replace PostgreSQL with MongoDB? Just write a new adapter. The business logic won't even notice the substitution.
\n\nIn Kotlin, this is especially convenient thanks to interfaces with default implementations, sealed classes, and functional style. Let's get down to business.
\n\nDesigning the System: An Online Store Example
\n\nLet's say we are writing a microservice for order management. At the center is OrderService, which creates orders and checks product availability. The outside world is an HTTP controller and a database.
Here is what the project structure looks like:
\n\nsrc/\n├── domain/ # Core (business logic)\n│ ├── model/\n│ ├── port/\n│ └── service/\n├── adapter/\n│ ├── input/ # Incoming adapters (controllers)\n│ └── output/ # Outgoing adapters (repositories)\n└── Application.kt # Entry point\n\n\nStep 1. Core: Models and Ports
\n\nLet's start with the order model. In Kotlin, this will be a data class:
// domain/model/Order.kt\ndata class Order(\n val id: String,\n val customerId: String,\n val items: List<OrderItem>,\n val status: OrderStatus\n)\n\nenum class OrderStatus {\n PENDING, CONFIRMED, SHIPPED, CANCELLED\n}\n\ndata class OrderItem(\n val productId: String,\n val quantity: Int\n)\n\n\nNow let's define the ports—interfaces through which the core communicates with the outside world. We will have two ports: one for retrieving product data, and another for saving orders.
\n\n// domain/port/ProductRepository.kt\ninterface ProductRepository {\n fun checkAvailability(productId: String, quantity: Int): Boolean\n}\n\n// domain/port/OrderRepository.kt\ninterface OrderRepository {\n fun save(order: Order): Order\n fun findById(id: String): Order?\n}\n\n\nNotice: there is no mention of a database, HTTP, or frameworks anywhere. Pure business logic.
\n\nStep 2. Business Service
\n\nNow let's write OrderService, which uses these ports. In Kotlin, we pass dependencies through the constructor—this is called dependency injection (DI).
// domain/service/OrderService.kt\nclass OrderService(\n private val productRepository: ProductRepository,\n private val orderRepository: OrderRepository\n) {\n fun createOrder(customerId: String, items: List<OrderItem>): Order {\n // Check availability of each product\n for (item in items) {\n if (!productRepository.checkA