Let's dive into the Hexagonal Architecture, also known as the Ports and Adapters architecture. It’s a popular system design pattern used for creating maintainable, flexible, and decoupled software applications.
The Hexagonal Architecture is a software architecture pattern that promotes separation of concerns by isolating the core logic of your application (called the Domain) from external systems like databases, UIs, APIs, or other services using Ports and Adapters.
I'll present this as a colorful visual layout, which you can imagine or draw:
[ External World: CLI / Web / Mobile App / API ]
|
[Adapter]
|
[ Port ]
|
┌────────────────┐
│ Application │
│ Core │
│ (Domain Logic) │
└────────────────┘
|
[ Port ]
|
[Adapter: DB / External APIs]
💡 What it is: The heart of the system that contains business rules and logic.
❌ No dependencies on external frameworks.
✅ Pure Python (or any language) classes and functions.
🔒 Keeps your system robust and testable.
🌀 What it is: Interfaces that define how the application talks to the outside world.
🔁 Inbound Ports: Accept inputs (e.g., service interfaces, commands).
📤 Outbound Ports: Define what the core needs from external systems (e.g., database read/write, API calls).
Think of Ports as plugs — defining what needs to be connected without caring how it works.
🔧 What it is: Implementations of Ports.
✨ Translates between the format used by external systems (REST, SQL, UI events) and what the core logic understands.
🧱 Inbound Adapters: e.g., Controllers, CLI, Event Handlers.
🧱 Outbound Adapters: e.g., Repositories, Web Clients, Message Brokers.
Feature | Benefit |
---|---|
🔄 Loose Coupling | Easily swap UI, DB, or external services |
🧪 Testability | Core can be tested in isolation |
📦 Plug-and-Play | Add new interfaces without changing core logic |
⚙️ Maintainability | Easier to manage and evolve over time |
# port interface
class AccountService:
def deposit(self, account_id: str, amount: float): pass
def withdraw(self, account_id: str, amount: float): pass
@app.route('/deposit', methods=['POST'])
def deposit():
account_id = request.json['account_id']
amount = request.json['amount']
account_service.deposit(account_id, amount)
return "Success"
class AccountRepository:
def save(account): ...
def load(account_id): ...
🎨 Layer | 🔍 Responsibility | 🔌 Examples |
---|---|---|
🧠 Core | Business rules | Account , TransactionService |
🚪 Ports | Define how core talks to outside | IAccountRepository , IServicePort |
🔧 Adapters | Implement interfaces | REST Controller, SQLAlchemy Repository |
🌍 External | Tools/Interfaces that use the app | UI, DB, External APIs |
Dependency Inversion: Core doesn't depend on external layers.
Open-Closed Principle: Add functionality via new adapters, not modifying core.
DRY and SRP: Keeps each part of the system focused.
✅ Recommended for:
Large or long-term systems
Systems with multiple interfaces (e.g., API + CLI + Scheduler)
Domain-driven applications
Systems requiring testability