In the rapidly evolving landscape of software development, building applications that are not only functional but also maintainable, testable, and adaptable to change is paramount. This is where architectural patterns like Hexagonal Architecture, also known as Ports and Adapters, come into play. It offers a structured approach to designing software that keeps your core business logic isolated from external concerns, making your systems more robust and easier to evolve.
This article will guide you through implementing Hexagonal Architecture using Python and FastAPI, a modern, fast (high-performance) web framework for building APIs. We’ll explore its core principles, demonstrate its application with a real-world business example, and highlight why this combination is a powerful choice for developing scalable and resilient applications in the US tech market.
Understanding Hexagonal Architecture
Hexagonal Architecture, introduced by Alistair Cockburn, is a design pattern that aims to create loosely coupled application components. Its primary goal is to allow an application to be equally driven by users, programs, automated tests, or batch scripts, and to be developed and tested in isolation from its run-time devices and databases.
What is Hexagonal Architecture?
Imagine your application’s core business logic as the ‘hexagon’ at the center. This hexagon doesn’t know anything about the outside world – whether it’s a web interface, a database, or another service. Instead, it interacts with the outside world through clearly defined ‘ports’. These ports are like sockets that external components (‘adapters’) can plug into. This isolation ensures that changes in external technologies (like switching databases or frontend frameworks) do not require changes to your core business rules.
Hexagonal Architecture emphasizes the strict separation of concerns, ensuring that the domain logic remains pure and untainted by infrastructure details. This leads to systems that are easier to understand, maintain, and adapt.
Key Principles
The strength of Hexagonal Architecture lies in its adherence to several fundamental design principles:
- Separation of Concerns: The application is divided into distinct layers, primarily separating the core business logic from technical infrastructure.
- Dependency Inversion Principle (DIP): High-level modules (your domain) should not depend on low-level modules (your infrastructure). Both should depend on abstractions. This means your core defines interfaces (ports), and external components implement these interfaces (adapters).
- Infrastructure Agnosticism: The core business logic should be entirely independent of the specific technologies used for persistence, UI, or external services.
These principles work together to create an architecture where the core domain logic is protected and can evolve independently, significantly reducing technical debt and increasing flexibility.
Core Components Explained
To implement Hexagonal Architecture effectively, it’s crucial to understand its primary components:
- Application Core (Domain Layer): This is the heart of your application. It contains all the business rules, entities, value objects, and use cases (application services). It’s technology-agnostic and should be the most stable part of your system.
- Ports: These are abstract interfaces that define the communication contracts between the application core and the outside world. There are two types:
- Driving Ports (API Ports): These define how the outside world interacts with the core. For example, an interface for a customer service that allows creating or retrieving customers.
- Driven Ports (SPI Ports): These define how the core interacts with external systems. For example, an interface for a customer repository that defines methods for saving or fetching customer data.
- Adapters: These are concrete implementations of the ports. They translate specific technology details to the generic interfaces defined by the ports.
- Driving Adapters: Implement driving ports to allow external agents to interact with the core. Examples include a FastAPI controller, a CLI command, or a message queue consumer.
- Driven Adapters: Implement driven ports to allow the core to interact with external systems. Examples include a SQLAlchemy repository for a database, an HTTP client for an external API, or a Kafka producer.
The beauty of this setup is that the application core only ever depends on its own abstractions (ports), never on concrete adapters. This makes it incredibly flexible.

Why Hexagonal Architecture with Python and FastAPI?
Python’s versatility and FastAPI’s performance make them an excellent combination for building modern web services. Integrating Hexagonal Architecture elevates these tools to create truly enterprise-grade solutions.
FastAPI’s Role as a Driving Adapter
FastAPI is a natural fit for serving as a driving adapter. Its features align perfectly with the needs of a hexagonal application:
- Asynchronous Nature: FastAPI is built on Starlette and Pydantic, supporting asynchronous operations out of the box, which is crucial for high-performance APIs.
- Pydantic for Data Validation: Pydantic models can be used to define the input and output schemas for your API, acting as the data contracts at the edge of your application. These models can then be mapped to your domain entities.
- Dependency Injection System: FastAPI’s robust dependency injection system makes it straightforward to inject your application services (use cases) and repository implementations into your API endpoints, wiring up the hexagon.
Your FastAPI endpoints essentially become the entry points, or the ‘driving side’, that trigger actions within your application core.
Python’s Suitability
Python’s features make it well-suited for implementing Hexagonal Architecture:
- Readability and Expressiveness: Python’s clean syntax helps in clearly defining domain logic and interfaces.
- Abstract Base Classes (ABCs): Python’s
abcmodule allows you to define abstract interfaces (ports) that concrete adapters must implement, enforcing the contracts. - Type Hinting: While Python is dynamically typed, type hints greatly enhance code clarity and maintainability, especially in larger projects, helping to define the boundaries of your hexagon.
Advantages in a Business Context (US Market focus)
Adopting Hexagonal Architecture with Python and FastAPI offers significant business advantages, particularly for companies operating in competitive US tech markets:
- Reduced Technical Debt: By clearly separating concerns, the architecture prevents the accumulation of technical debt. Changes in infrastructure technology don’t necessitate rewriting business logic, saving valuable development time and resources.
- Enhanced Testability: The core business logic can be tested in isolation without needing to set up databases, web servers, or external services. This leads to faster, more reliable unit tests and higher confidence in your codebase.
- Faster Feature Development: Developers can focus on implementing business rules without getting entangled in infrastructure details. This agility allows businesses to bring new features to market more quickly, gaining a competitive edge.
- Easier Technology Swaps: Need to migrate from PostgreSQL to MongoDB? Or switch from one payment gateway to another? With Hexagonal Architecture, you only need to write a new adapter for the driven port, leaving your core business logic untouched. This flexibility is invaluable for long-term scalability and adapting to changing market demands.
Designing a Hexagonal Application: A Customer Management Example
Let’s illustrate Hexagonal Architecture with a common business scenario: a Customer Relationship Management (CRM) module. We’ll focus on the core functionalities of managing customer data.
Business Scenario: Customer Relationship Management (CRM)
Our goal is to build a microservice that handles customer operations. Specifically, we need to:
- Create new customer records.
- Retrieve existing customer details by ID.
- Update customer information.
- Store customer data persistently in a database (e.g., PostgreSQL).
- Expose these functionalities via a RESTful API.
Defining the Domain Layer (Application Core)
The domain layer is where the business rules live. It should be independent of any specific framework or database.
customer.py: Defines theCustomerentity and potentially value objects.customer_service.py: Contains the application services (use cases) likecreate_customer,get_customer,update_customer. These orchestrate interactions with domain entities and driven ports.customer_repository.py: This will be our driven port, an abstract interface for data persistence.
Implementing Ports
Ports are Python Abstract Base Classes (ABCs) that define the contracts.
# src/domain/ports.py
from abc import ABC, abstractmethod
from typing import List, Optional
from src.domain.entities import Customer
class CustomerRepository(ABC):
"""Abstract base class for customer data persistence."""
@abstractmethod
async def save(self, customer: Customer) -> None:
"""Saves a new customer or updates an existing one."""
pass
@abstractmethod
async def get_by_id(self, customer_id: str) -> Optional[Customer]:
"""Retrieves a customer by their ID."""
pass
@abstractmethod
async def get_all(self) -> List[Customer]:
"""Retrieves all customers."""
pass
class NotificationService(ABC):
"""Abstract base class for sending notifications."""
@abstractmethod
async def send_email(self, recipient: str, subject: str, body: str) -> None:
"""Sends an email notification."""
pass
Creating Adapters
Adapters are concrete implementations of the ports, connecting the core to specific technologies.
- Persistence Adapter: An implementation of
CustomerRepositoryusing SQLAlchemy for a relational database. - API Adapter: A FastAPI application that serves as the driving adapter, consuming requests and calling the application services.

Code Walkthrough: Building the Hexagon
Let’s lay out the project structure and dive into the code for our customer management system.
Project Structure
A well-organized project structure is key to maintaining clarity in a hexagonal architecture.
. PROJECT_ROOT
├── src/
│ ├── domain/ # The application core (business logic)
│ │ ├── entities.py # Customer, Address, etc.
│ │ ├── ports.py # Abstract interfaces (CustomerRepository, NotificationService)
│ │ └── services.py # Application services / Use cases (CustomerService)
│ ├── adapters/ # Implementations of ports (infrastructure details)
│ │ ├── persistence/ # Driven adapters for data storage
│ │ │ ├── sqlalchemy_repository.py # SQLAlchemy implementation of CustomerRepository
│ │ │ └── database.py # SQLAlchemy engine/session setup
│ │ │ └── __init__.py
│ │ └── web/ # Driving adapters for web interfaces
│ │ ├── fastapi_adapter.py # FastAPI routes and dependency injection
│ │ └── __init__.py
│ └── __init__.py
├── main.py # Application entry point, wiring up dependencies
├── requirements.txt
└── tests/
├── unit/ # Tests for domain logic (no external dependencies)
├── integration/ # Tests for adapters (e.g., persistence with a real DB)
└── e2e/ # End-to-end tests (via FastAPI)
Domain Layer (Core)
First, define the core entities and services.
# src/domain/entities.py
from pydantic import BaseModel
from typing import Optional
class Customer(BaseModel):
id: Optional[str] = None # Will be generated by persistence layer or UUID
name: str
email: str
address: Optional[str] = None
def update_details(self, name: Optional[str] = None,
email: Optional[str] = None,
address: Optional[str] = None):
"""Updates customer details, encapsulating business logic."""
if name: self.name = name
if email: self.email = email
if address: self.address = address
# Add more complex business rules here if needed, e.g., email validation
# src/domain/services.py
from uuid import uuid4
from typing import List, Optional
from src.domain.entities import Customer
from src.domain.ports import CustomerRepository, NotificationService
class CustomerService:
"""Application service (use case) for managing customers.
It orchestrates domain entities and interacts with driven ports.
"""
def __init__(self, customer_repo: CustomerRepository,
notification_service: NotificationService):
self.customer_repo = customer_repo
self.notification_service = notification_service
async def create_customer(self, name: str, email: str, address: Optional[str] = None) -> Customer:
customer = Customer(id=str(uuid4()), name=name, email=email, address=address)
await self.customer_repo.save(customer)
await self.notification_service.send_email(
recipient=customer.email,
subject="Welcome to Our Service!",
body=f"Dear {customer.name}, welcome aboard!"
)
return customer
async def get_customer_by_id(self, customer_id: str) -> Optional[Customer]:
return await self.customer_repo.get_by_id(customer_id)
async def update_customer(self, customer_id: str,
name: Optional[str] = None,
email: Optional[str] = None,
address: Optional[str] = None) -> Optional[Customer]:
customer = await self.customer_repo.get_by_id(customer_id)
if not customer:
return None
customer.update_details(name, email, address) # Business logic encapsulated in entity
await self.customer_repo.save(customer)
return customer
async def get_all_customers(self) -> List[Customer]:
return await self.customer_repo.get_all()
Persistence Adapter (Driven Port Implementation)
Here’s how we implement the CustomerRepository using SQLAlchemy. This adapter handles the specifics of database interaction.
# src/adapters/persistence/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy import Column, String
DATABASE_URL = "sqlite+aiosqlite:///./sql_app.db" # For simplicity, using SQLite
# In production, this would be a PostgreSQL or similar connection string
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
Base = declarative_base()
class CustomerORM(Base):
__tablename__ = "customers"
id = Column(String, primary_key=True, index=True)
name = Column(String, index=True)
email = Column(String, unique=True, index=True)
address = Column(String, nullable=True)
# src/adapters/persistence/sqlalchemy_repository.py
from typing import List, Optional
from uuid import uuid4
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities import Customer
from src.domain.ports import CustomerRepository
from src.adapters.persistence.database import CustomerORM
class SQLAlchemyCustomerRepository(CustomerRepository):
"""SQLAlchemy implementation of the CustomerRepository port."""
def __init__(self, session: AsyncSession):
self.session = session
async def save(self, customer: Customer) -> None:
# Check if customer exists to determine insert or update
existing_customer_orm = await self.session.get(CustomerORM, customer.id)
if existing_customer_orm:
# Update existing customer
existing_customer_orm.name = customer.name
existing_customer_orm.email = customer.email
existing_customer_orm.address = customer.address
else:
# Create new customer
customer_orm = CustomerORM(
id=customer.id or str(uuid4()), # Ensure ID even if not provided by domain
name=customer.name,
email=customer.email,
address=customer.address
)
self.session.add(customer_orm)
await self.session.commit()
await self.session.refresh(existing_customer_orm or customer_orm)
async def get_by_id(self, customer_id: str) -> Optional[Customer]:
stmt = select(CustomerORM).where(CustomerORM.id == customer_id)
result = await self.session.execute(stmt)
customer_orm = result.scalar_one_or_none()
if customer_orm:
return Customer(
id=customer_orm.id,
name=customer_orm.name,
email=customer_orm.email,
address=customer_orm.address
)
return None
async def get_all(self) -> List[Customer]:
stmt = select(CustomerORM)
result = await self.session.execute(stmt)
return [
Customer(
id=c.id,
name=c.name,
email=c.email,
address=c.address
) for c in result.scalars().all()
]
# src/adapters/persistence/in_memory_repository.py (Example of another adapter)
# from typing import Dict, List, Optional
# from src.domain.entities import Customer
# from src.domain.ports import CustomerRepository
# class InMemoryCustomerRepository(CustomerRepository):
# """In-memory implementation for testing or simple cases."""
# def __init__(self):
# self._customers: Dict[str, Customer] = {}
# async def save(self, customer: Customer) -> None:
# self._customers[customer.id] = customer
# async def get_by_id(self, customer_id: str) -> Optional[Customer]:
# return self._customers.get(customer_id)
# async def get_all(self) -> List[Customer]:
# return list(self._customers.values())
Notice how SQLAlchemyCustomerRepository implements the CustomerRepository interface. If we wanted to switch to a NoSQL database, we’d simply create a new adapter (e.g., MongoDBCustomerRepository) that implements the same CustomerRepository port, without touching the CustomerService or any other domain logic.
Web Adapter (Driving Port Implementation)
The FastAPI application serves as the driving adapter. It receives HTTP requests, maps them to domain commands, and orchestrates the application service.
# src/adapters/web/fastapi_adapter.py
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional
from src.domain.entities import Customer
from src.domain.services import CustomerService
# Pydantic models for API request/response (DTOs - Data Transfer Objects)
class CustomerCreateRequest(BaseModel):
name: str
email: str
address: Optional[str] = None
class CustomerUpdateRequest(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
address: Optional[str] = None
class CustomerResponse(BaseModel):
id: str
name: str
email: str
address: Optional[str] = None
class Config:
orm_mode = True # Allows Pydantic to read ORM models
router = APIRouter()
# Dependency to inject CustomerService
# This will be provided by main.py using FastAPI's Depends
async def get_customer_service(customer_service: CustomerService = Depends()) -> CustomerService:
return customer_service
@router.post("/customers", response_model=CustomerResponse, status_code=status.HTTP_201_CREATED)
async def create_customer_endpoint(
request: CustomerCreateRequest,
customer_service: CustomerService = Depends(get_customer_service)
):
customer = await customer_service.create_customer(**request.dict())
return customer
@router.get("/customers/{customer_id}", response_model=CustomerResponse)
async def get_customer_endpoint(
customer_id: str,
customer_service: CustomerService = Depends(get_customer_service)
):
customer = await customer_service.get_customer_by_id(customer_id)
if not customer:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
return customer
@router.put("/customers/{customer_id}", response_model=CustomerResponse)
async def update_customer_endpoint(
customer_id: str,
request: CustomerUpdateRequest,
customer_service: CustomerService = Depends(get_customer_service)
):
customer = await customer_service.update_customer(customer_id, **request.dict(exclude_unset=True))
if not customer:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
return customer
@router.get("/customers", response_model=List[CustomerResponse])
async def get_all_customers_endpoint(
customer_service: CustomerService = Depends(get_customer_service)
):
customers = await customer_service.get_all_customers()
return customers
Putting it Together (Dependency Injection)
The main.py file is responsible for wiring up all the components using FastAPI’s dependency injection system. This is where we create concrete instances of our adapters and inject them into our application services.
# main.py
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.services import CustomerService
from src.adapters.persistence.sqlalchemy_repository import SQLAlchemyCustomerRepository
from src.adapters.persistence.database import AsyncSessionLocal, engine, Base, CustomerORM
from src.adapters.web.fastapi_adapter import router as customer_router
from src.domain.ports import NotificationService # Also need a dummy for now
# Dummy Notification Service for demonstration
class DummyNotificationService(NotificationService):
async def send_email(self, recipient: str, subject: str, body: str) -> None:
print(f"[Dummy Email Sent] To: {recipient}, Subject: {subject}, Body: {body}")
app = FastAPI(title="Hexagonal Customer API")
# Dependency to get an async database session
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
# Dependency for CustomerRepository
async def get_customer_repository(db: AsyncSession = Depends(get_db)) -> SQLAlchemyCustomerRepository:
return SQLAlchemyCustomerRepository(db)
# Dependency for NotificationService
async def get_notification_service() -> DummyNotificationService:
return DummyNotificationService()
# Dependency for CustomerService (the application service)
async def get_customer_service(
customer_repo: SQLAlchemyCustomerRepository = Depends(get_customer_repository),
notification_service: DummyNotificationService = Depends(get_notification_service)
) -> CustomerService:
return CustomerService(customer_repo, notification_service)
# Override FastAPI's default dependency for CustomerService in the router
customer_router.dependency_overrides[CustomerService] = get_customer_service
# Include the API router
app.include_router(customer_router)
@app.on_event("startup")
async def startup_event():
# Create database tables on startup
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
@app.get("/")
async def read_root():
return {"message": "Welcome to the Hexagonal Customer API!"}
To run this application, you would typically install the dependencies (fastapi, uvicorn[standard], sqlalchemy, asyncpg or aiosqlite for database) and then run uvicorn main:app --reload.
Testing in a Hexagonal Architecture
One of the most significant benefits of Hexagonal Architecture is its impact on testability. The clear separation of concerns allows for highly effective testing strategies.
Unit Testing the Domain
Your domain entities and application services (use cases) can be unit tested in complete isolation. You don’t need a database, a web server, or any external service. You simply mock the ports (interfaces) that your services depend on.
- Example: Testing
CustomerService
You would mockCustomerRepositoryandNotificationService. This ensures that you are only testing the business logic withinCustomerService, not the persistence or notification mechanisms.
# tests/unit/test_customer_service.py
import pytest
from unittest.mock import AsyncMock
from src.domain.entities import Customer
from src.domain.ports import CustomerRepository, NotificationService
from src.domain.services import CustomerService
@pytest.mark.asyncio
async def test_create_customer():
mock_repo = AsyncMock(spec=CustomerRepository)
mock_notification = AsyncMock(spec=NotificationService)
service = CustomerService(mock_repo, mock_notification)
customer = await service.create_customer("Jane Doe", "jane@example.com")
assert customer.name == "Jane Doe"
assert customer.email == "jane@example.com"
mock_repo.save.assert_called_once()
mock_notification.send_email.assert_called_once_with(
recipient="jane@example.com",
subject="Welcome to Our Service!",
body=f"Dear Jane Doe, welcome aboard!"
)
@pytest.mark.asyncio
async def test_get_customer_by_id_found():
mock_repo = AsyncMock(spec=CustomerRepository)
mock_notification = AsyncMock(spec=NotificationService)
service = CustomerService(mock_repo, mock_notification)
expected_customer = Customer(id="123", name="John Doe", email="john@example.com")
mock_repo.get_by_id.return_value = expected_customer
customer = await service.get_customer_by_id("123")
assert customer == expected_customer
mock_repo.get_by_id.assert_called_once_with("123")
Integration Testing Adapters
Adapters should be tested to ensure they correctly interact with their specific technologies. For example, the SQLAlchemyCustomerRepository should be tested against a real (or in-memory) database instance to confirm it can save and retrieve data correctly.
End-to-End Testing
Finally, end-to-end tests verify the entire system, from the API endpoint through the application core to the database and back. FastAPI’s TestClient is excellent for this.
Real-World Business Impact and Considerations
Adopting Hexagonal Architecture is not just a technical decision; it has profound implications for how businesses build and maintain software, especially in dynamic markets like the US.
Scalability and Microservices
Hexagonal Architecture inherently promotes modularity, making it an ideal foundation for microservice architectures. Each microservice can itself be a hexagonal application, offering clear boundaries and independent deployability. This approach allows organizations to scale specific services independently, optimizing resource utilization and performance.
Team Collaboration
The clear separation between the domain layer and infrastructure layers fosters better team collaboration. Domain experts and business analysts can focus on defining and refining the core business logic, while infrastructure teams can work on optimizing persistence, messaging, and UI details. This parallel development reduces bottlenecks and improves overall project velocity.
Cost Savings (US perspective)
For US businesses, efficiency often translates directly to cost savings. By reducing technical debt and improving testability, Hexagonal Architecture leads to:
- Reduced Maintenance Costs: Fewer bugs, easier debugging, and simpler refactoring mean less time and money spent on maintaining existing systems.
- Faster Time-to-Market: The ability to quickly develop and deploy new features or adapt to changing requirements means businesses can respond faster to market opportunities, potentially generating revenue sooner.
- Lower Risk of Vendor Lock-in: The infrastructure agnosticism reduces the risk and cost associated with being locked into a particular vendor or technology, providing greater strategic flexibility.
Potential Trade-offs
While beneficial, Hexagonal Architecture is not a silver bullet. There are some trade-offs to consider:
- Initial Learning Curve and Boilerplate: For teams new to the pattern, there might be an initial learning curve. The need to define ports and adapters can also introduce more files and a bit more boilerplate code compared to a simpler, more tightly coupled architecture.
- Over-engineering for Small Applications: For very small, simple applications with minimal business logic and unlikely future changes, the overhead of Hexagonal Architecture might be an overkill. It shines brightest in complex, evolving systems where maintainability and flexibility are critical.

Conclusion
Hexagonal Architecture, when combined with the power of Python and FastAPI, provides a robust blueprint for building applications that are resilient, scalable, and easy to maintain. By strictly separating your core business logic from external concerns, you create a system that is highly testable, adaptable to technological changes, and less prone to technical debt. While it requires an upfront investment in understanding and structuring your code, the long-term benefits in terms of flexibility, development speed, and reduced operational costs far outweigh the initial effort. For any US business looking to build high-quality, future-proof software, embracing Hexagonal Architecture is a strategic move towards sustainable success.