Introduction
In our previous lesson, we established the fundamentals of Layered Architecture, focusing on separating concerns into distinct vertical layers and enforcing a unidirectional flow of dependencies. We saw how this protects the application's core logic. Now, we will build directly on that foundation by exploring a pattern that refines and visualizes this separation in a powerful way: Hexagonal Architecture, also known as Ports and Adapters.
This lesson isn't about learning a completely new paradigm, but rather a different and often stricter way of thinking about the same core problem: isolating your application's domain from the outside world. Instead of a vertical stack of layers, we'll envision our application as a central core (the hexagon) communicating with external tools and technologies through well-defined "ports." Your goal for today is to learn how to design an application using this inside-out perspective, defining the contracts (ports) and the concrete implementations (adapters) that fulfill them.
From Layers to Hexagons
The core idea of Hexagonal Architecture, first described by Alistair Cockburn, is to create a sharp boundary between your application's core logic and the external services it interacts with. These external components could be anything from a user interface, a database, a message queue, or another application.
The architecture is composed of three main concepts:
-
The Application Core (The Hexagon): This is the heart of your software. It contains the business rules, policies, and logic that are essential to the domain you're modeling. Crucially, it has no knowledge of the outside world. It doesn't know about databases, web frameworks, or message brokers. This corresponds to the Application and Domain layers from our previous lesson.
-
Ports: These are the interfaces that define how the application core can be communicated with and what it needs from the outside. They are part of the application core and dictate the terms of engagement. A port is simply a contract, like a Rust
traitor a PythonProtocol. -
Adapters: These are the concrete implementations that connect the outside world to the ports. They translate the specific technology's API into the application-agnostic language of the port.
This "inside-out" dependency rule is the pattern's defining characteristic. Everything depends on the core; the core depends on nothing.

Primary and Secondary Actors: Driving vs. Driven
To make this more concrete, we can categorize ports and adapters into two types:
-
Primary (or Driving): These are actors that drive the application and initiate an action.
- Primary Port (Input Port): An interface that defines a use case of your application, for example, a
CreateOrderUseCasetrait. It's implemented by the application core. - Primary Adapter (Input Adapter): The code that calls the input port. A FastAPI controller, a gRPC handler, or a CLI command are all primary adapters. They take a request from a user or another system and translate it into a call on a use case.
- Primary Port (Input Port): An interface that defines a use case of your application, for example, a
-
Secondary (or Driven): These are actors that are driven by the application to get things done.
- Secondary Port (Output Port): An interface that defines a service the application needs, like
OrderRepositoryorPaymentGateway. It is defined by the application core. - Secondary Adapter (Output Adapter): The code that implements the output port. A PostgreSQL repository implementation or a Stripe payment client are secondary adapters. They provide the concrete technology to fulfill the application's need.
- Secondary Port (Output Port): An interface that defines a service the application needs, like
The critical insight here is that the application core defines all the ports. It dictates the contracts for both how it is driven and what it needs to drive.
A Practical Walkthrough in Python
Let's refactor an application from a tangled mess into a clean, hexagonal design. A common anti-pattern is mixing business logic directly with framework code (e.g., FastAPI and SQLAlchemy).
Stop Mixing FastAPI with Business Logic: Fix It with Ports & Adapters
ArjanCodes' video provides a perfect starting point. Watch the introduction where he shows a FastAPI endpoint with embedded SQL queries and business checks.
Watch the initial problem description from the beginning until the explanation of the "complete mess." This highlights the tight coupling we aim to solve.
To fix this, we'll apply the hexagonal pattern step-by-step.
Stop Mixing FastAPI with Business Logic: Fix It with Ports & Adapters
The rest of this video is an excellent, hands-on demonstration of building a hexagonal application in Python.
Conceptual Overview: First, watch the explanation of the three layers (Domain, Ports, Adapters) from this section. Define the Domain: See how he creates a domain folder and defines framework-independent models and errors. This isolates the core language of the application. Watch from this part. Define the Output Port: Next, he defines an InventoryPort using a Python Protocol. This is the contract the domain needs for persistence, without knowing how it's persisted. Watch from this segment. Implement the Use Case (Input Port): He then rewrites the place_order function as a pure use case that depends only on the domain models and the InventoryPort. This function is the implementation of our primary/input port. Watch from this walkthrough. Create the Output Adapter: Now, an adapter is created to bridge the gap. A SQLAlchemyInventoryAdapter is built to implement the InventoryPort protocol using a specific database technology. Watch from this part. Create the Input Adapter & Compose: Finally, the FastAPI endpoint is refactored to be a clean input adapter. Its only jobs are to translate the HTTP request into a domain request, call the use case, and translate the domain result (or error) back into an HTTP response. This is also where the adapters are instantiated and "plugged into" the use case. Watch from this section.
The article "Hexagonal Architecture Practical Guide" provides an excellent project structure and a full Python example that reinforces these steps. I highly recommend reviewing its structure as a template for your own projects.
This article offers a clear, structured Python example that complements the video.
First, examine the project structure. Notice the clear separation of domain, ports, and adapters. Then, look at the config/dependency_injection.py file in the Dependency Injection section. This file is the "Composition Root," where the concrete adapters (PostgresOrderRepository, StripePaymentGateway) are created and injected into the OrderService. This is the final, crucial step that wires the whole application together.
A Deep Dive into Rust Implementation
The same principles apply directly to Rust, where traits are the natural tool for defining ports. The "Master Hexagonal Architecture in Rust" guide offers an exceptionally thorough walkthrough.
Master Hexagonal Architecture in Rust
This guide is a deep dive into building a hexagonal application in Rust. We'll walk through its core parts to see the transformation from a tightly-coupled to a loosely-coupled system.
The Problem: Start by reading Part I to understand the "Very Bad Application." Notice how the main function and the HTTP handler are deeply entangled with axum and sqlx. This sets the stage for the refactoring. Abstracting Persistence: Now read the beginning of Part II, up to the Implementing AuthorRepository subsection. Focus on how the AuthorRepository trait is defined as the output port and how domain models (Author, CreateAuthorRequest) are created. These are independent of any infrastructure. Refactoring the Adapters: Continue reading the rest of Part II. Here, you'll see the creation of an output adapter (Sqlite which implements AuthorRepository) and the refactoring of the input adapter (the create_author HTTP handler). The handler now depends on the AuthorRepository trait, not a concrete database type, making it clean and testable. Defining the Core Use Case: Next, read Part III, up to the "The Service trait keeps your unit test surface area sane" quote. This introduces the AuthorService trait, which acts as the input port. Its implementation orchestrates calls to multiple output ports (repository, metrics, etc.), encapsulating the entire business use case. Composition and Final Theory: Finally, skim through the main is for bootstrapping section to see how main becomes the composition root. Then, read the Why hexagons? section for the theoretical explanation that ties it all together.
Applying Hexagonal Architecture to a Computer Vision Pipeline
Your background in computer vision provides a great context for applying these ideas. Let's analyze a typical CV pipeline through a hexagonal lens.

Let's map the components of this pipeline to a hexagonal design:
-
Application Core (The Hexagon): The core business logic is the CV processing itself. This includes:
- The
Real-Time CVmodule containing theCV Modelsand event filtering logic. - The
VSS Multimodal Data Processorwhich handles stream chunks and frame selection. - The business logic within the
Alert BridgeandPriority Queue. - Domain Models:
ImageFrame,Detection,FeatureVector,QualifiedAlert. - Use Cases (Input Ports):
ProcessStreamUseCase,QualifyAlertUseCase.
- The
-
Ports (The Contracts):
- Input Ports: Interfaces like
ProcessStream(stream_id)orInvestigateEvent(event_id). - Output Ports: Interfaces the core needs, such as
FrameRepository(to get frames from a source),EventPublisher(to send raw detections),VLMProvider(to get qualifications), andAlertNotifier.
- Input Ports: Interfaces like
-
Adapters (The Implementations):
- Input Adapters: The code that pulls from
Camera Streamsand callsProcessStreamUseCase. An API endpoint on theDashboardthat allows a user to trigger a re-analysis would also be an input adapter. - Output Adapters: The concrete implementation that communicates with the
VMSto get video clips. TheVLM ToolandVLM NIMare complex secondary adapters that implement theVLMProviderport. The code that writes to theDashboard DBis another secondary adapter.
- Input Adapters: The code that pulls from
By structuring the pipeline this way, you could swap out the camera source (e.g., from a live RTSP stream to reading from a file) by simply writing a new adapter for the FrameRepository port, without touching any of the core CV logic. Similarly, you could change how alerts are sent (e.g., from a dashboard notification to an SMS message) by swapping AlertNotifier adapters. This architectural choice provides immense flexibility and maintainability for complex systems like industrial CV pipelines.
Conclusion
Today, we've built upon the principles of layered design to understand Hexagonal Architecture. It provides a powerful mental model for creating a robust separation between your application's core logic and the tools it uses to interact with the world.
Key Takeaways:
- Hexagonal Architecture (or Ports and Adapters) isolates the application core from external concerns like UIs, databases, and third-party services.
- All dependencies point inward toward the core (the hexagon). The core is completely independent.
- Ports are the interfaces defined by the core that act as contracts for communication.
- Input Ports (Use Cases) define how the application is driven.
- Output Ports (Repository/Gateway Interfaces) define what the application needs.
- Adapters are the concrete implementations that plug into the ports.
- Input Adapters (e.g., REST controllers, CLI commands) drive the application.
- Output Adapters (e.g., database clients, API clients) are driven by the application.
- This pattern leads to highly maintainable and testable systems, as the core business logic can be tested in complete isolation from any infrastructure.
In our next lesson, we will explore Clean Architecture. You'll find it shares the exact same goals and principles as Hexagonal Architecture. It is often seen as a more detailed specification, providing further guidance on how to structure the layers inside the hexagon itself.