Mastering Ports and Adapters Pattern in Python
Learn how to build decoupled and testable Python applications using the Hexagonal Architecture (Ports and Adapters) pattern.
In modern software development, maintaining a clean separation between business logic and external dependencies (like databases, APIs, or UIs) is crucial for building scalable and testable systems. One of the most effective patterns for achieving this is the Ports and Adapters pattern, also known as Hexagonal Architecture.
In this post, we’ll explore how to implement this pattern in Python using simple, practical examples.
What is Ports and Adapters?
The core idea is to put your Domain Logic at the center of the application. Everything else—databases, message brokers, web frameworks—is considered an “external” detail.
- Ports: These are interfaces that define how the outside world can interact with the domain or how the domain interacts with the outside world.
- Adapters: These are concrete implementations of the ports. For example, a PostgreSQL adapter for a database port, or a FastAPI adapter for a web entry point.
Why use it in Python?
Python’s dynamic nature and support for Abstract Base Classes (ABCs) make it an excellent fit for this architecture. It allows you to:
- Swap dependencies easily (e.g., switching from SQLite to PostgreSQL).
- Test in isolation by using mocks or in-memory adapters.
- Focus on Business Logic without worrying about framework boilerplate.
A Practical Example: User Registration
Let’s build a simple system to register users.
1. The Domain Model
First, we define our core entity. This is pure Python and has no dependencies.
from dataclasses import dataclass
@dataclass
class User:
username: str
email: str
2. The Port (Interface)
We define a “Port” for our repository using Python’s abc module. This tells the domain what it needs from a storage system without specifying which system.
from abc import ABC, abstractmethod
class UserRepositoryPort(ABC):
@abstractmethod
def save(self, user: User) -> None:
pass
@abstractmethod
def get_by_username(self, username: str) -> User:
pass
3. The Domain Service (The “Core”)
The service coordinates the logic. It depends on the Port, not a concrete implementation. This is Dependency Inversion in action.
class UserService:
def __init__(self, repository: UserRepositoryPort):
self.repository = repository
def register_user(self, username: str, email: str) -> None:
user = User(username=username, email=email)
self.repository.save(user)
print(f"User {username} registered successfully!")
4. The Adapter (Implementation)
Now we create a concrete “Adapter”. For testing, we might use an in-memory repository.
class InMemoryUserRepository(UserRepositoryPort):
def __init__(self):
self._users = {}
def save(self, user: User) -> None:
self._users[user.username] = user
def get_by_username(self, username: str) -> User:
return self._users.get(username)
5. Putting it Together
Finally, we “plug” our adapter into the service.
# Bootstrapping the application
repo = InMemoryUserRepository()
service = UserService(repository=repo)
# Using the system
service.register_user("pythonista", "hello@python.org")
Conclusion
The Ports and Adapters pattern helps you build “Future-Proof” applications. By isolating your business logic from the “noise” of frameworks and databases, you create a system that is easier to maintain, test, and evolve.
In the Python world, where libraries change fast, this architecture is a superpower. Give it a try in your next project!
What do you think about Hexagonal Architecture? Have you used it in your Odoo or FastAPI projects?
Enjoyed this read?
Join our newsletter for more engineering insights.