Hexagonal Architecture in C++: How to Stop Fearing Changes and Love Tests
\n\nHave you ever rewritten a database module, only to realize you need to change half of your business logic? Or tried to unit test a class that drags in three tons of external dependencies? If so — welcome to the club. Fortunately, we have a tool that solves these problems once and for all: hexagonal architecture (also known as Ports & Adapters architecture).
\n\nIn this article, we'll break down how to apply this powerful concept in C++. You'll see not abstract diagrams, but real code you can use right now. And yes, we'll do it without fanaticism — so that neither your legacy project explodes nor your new code becomes messy.
\n\nWhat is Hexagonal Architecture (and What Does the Hexagon Have to Do With It)?
\n\nImagine the core of your application — it's pure business logic that knows nothing about databases, REST APIs, or graphical interfaces. Around this core are ports (interfaces), and further out are adapters (concrete implementations). Visually, this resembles a hexagon, hence the name.
\n\nThe key idea: dependencies point inward. The outside world depends on the core, not the other way around. This allows you to change the database, communication protocol, or UI without modifying the business logic. Sounds like a dream? Let's see how to implement this in C++.
\n\nThe Classic Problem: Tight Coupling
\n\nHere's what typical C++ code often looks like:
\n\nclass OrderService {\npublic:\n void createOrder(const OrderData& data) {\n // Business logic\n if (data.items.empty()) throw std::runtime_error("Order is empty");\n\n // And here's the problem — direct dependency on the database\n Database db("localhost", 5432);\n db.connect();\n db.saveOrder(data);\n db.disconnect();\n \n // And also on the email service\n EmailSender sender("smtp.example.com");\n sender.sendInvoice(data);\n }\n};\n\nWhat's wrong here? Everything. You cannot test createOrder without a real database and mail server. If tomorrow you decide to switch from PostgreSQL to MongoDB — you'll have to rewrite OrderService. And if the email service becomes asynchronous via a queue — more changes to the business logic.
Solving the Problem with Hexagonal Architecture
\n\nStep 1: Define Ports (Interfaces)
\n\nA port is a contract that defines what the core needs from the outside world, but not how it's implemented.
\n\n// port/OrderRepository.h\nclass OrderRepository {\npublic:\n virtual ~OrderRepository() = default;\n virtual void save(const Order& order) = 0;\n virtual std::optional<Order> findById(const std::string& id) = 0;\n};\n\n// port/NotificationService.h\nclass NotificationService {\npublic:\n virtual ~NotificationService() = default;\n virtual void sendInvoice(const Order& order) = 0;\n};\n\nNotice: these are pure abstract classes. No dependencies on frameworks, libraries, or external services. Only business entities.
\n\nStep 2: Write the Core (Business Logic)
\n\nNow our service depends only on interfaces, not on concrete implementations:
\n\n// core/OrderService.h\nclass OrderService {\npublic:\n OrderService(OrderRepository& repo, NotificationService& notifier)\n : repository_(repo), notifier_(notifier) {}\n\n void createOrder(const OrderData& data) {\n // Validation — pure business logic\n if (data.items.empty()) {\n throw std::invalid_argument("Order cannot be empty");\n }\n\n Order order = Order::create(data);\n repository_.save(order);\n notifier_.sendInvoice(order);\n }\n\nprivate:\n OrderRepository& repository_;\n NotificationServ