In today’s fast-paced software development landscape, building applications that are easy to maintain, adapt, and scale is crucial. Hexagonal Architecture (also known as Ports and Adapters) and Domain-Driven Design (DDD) are an effective combo for addressing these challenges. Hexagonal Architecture promotes clean separation of concerns, making it easier to replace, test, or enhance parts of the system without disrupting the core logic. Meanwhile, DDD focuses on aligning your code with real-world business concepts, ensuring your system is both intuitive and resilient. Together, these approaches enable developers to build systems that are robust, resilient, and designed to seamlessly adapt to changing requirements and future growth.
Hexagonal Architecture, also known as the Ports and Adapters pattern, was introduced by Alistair Cockburn to address the rigidity and complexity of traditional layered architecture. Its primary goal is to make the application’s core logic (domain) independent of external systems, enabling easier testing, maintenance, and adaptability.
At its core, Hexagonal Architecture divides the application into three main layers:
Core (Business Logic/Domain): The heart of the system where business rules and domain logic reside. This layer is independent and does not rely on external libraries or frameworks.
Example: Calculating interest on a loan or validating a user’s action against business rules.
Ports (Interfaces): Abstract definitions (e.g., interfaces or protocols) for the ways the core interacts with the outside world. Ports represent use cases or application-specific APIs. They define what needs to be done without specifying how.
Example: Repository Port defines methods to interact with data sources like:
src/ports/repository.py from abc import ABC, abstractmethod from typing import List from src.entities import Entity class Repository(ABC): @abstractmethod def get(self, id: str) -> Entity: pass @abstractmethod def insert(self, entity: Entity) -> None: pass @abstractmethod def update(self, entity: Entity) -> None: pass
# src/adapters/postgres_repository.py from sqlalchemy import create_engine, Column, String from sqlalchemy.orm import declarative_base, sessionmaker from src.entities import Entity from src.ports.repository import Repository Base = declarative_base() # Define the database table for Entity class EntityModel(Base): __tablename__ = "entities" id = Column(String, primary_key=True) name = Column(String, nullable=False) description = Column(String) class PostgresRepository(Repository): def __init__(self, db_url: str): """ Initialize the repository with the PostgreSQL connection URL. Example db_url: "postgresql+psycopg2://username:password@host:port/dbname" """ self.engine = create_engine(db_url) Base.metadata.create_all(self.engine) self.Session = sessionmaker(bind=self.engine) def get(self, id: str) -> Entity: session = self.Session() try: entity_model = session.query(EntityModel).filter_by(id=id).first() if not entity_model: raise ValueError(f"Entity with id {id} not found") return Entity(id=entity_model.id, name=entity_model.name, description=entity_model.description) finally: session.close() def insert(self, entity: Entity) -> None: session = self.Session() try: entity_model = EntityModel(id=entity.id, name=entity.name, description=entity.description) session.add(entity_model) session.commit() finally: session.close() def update(self, entity: Entity) -> None: session = self.Session() try: entity_model = session.query(EntityModel).filter_by(id=entity.id).first() if not entity_model: raise ValueError(f"Entity with id {entity.id} not found") entity_model.name = entity.name entity_model.description = entity.description session.commit() finally: session.close()
The architecture is often visualized as a hexagon, symbolizing multiple ways to interact with the core, with each side representing a different adapter or port.
Domain-Driven Design (DDD) is a software design approach that emphasizes a close alignment between business goals and the software being built to achieve them. This methodology was introduced by Eric Evans in his book Domain-Driven Design: Tackling Complexity in the Heart of Software.
At its core, DDD focuses on understanding and modeling the domain (the business problem space) with the help of domain experts and translating that understanding into the software system. DDD promotes the decoupling of domains, ensuring that different parts of the system remain independent, clear, and easy to manage.
Key Concepts of Domain-Driven Design:
Domain: The specific area of knowledge or activity that the software addresses. For example, in a banking application, the domain includes concepts like accounts, transactions, and customers.
Ubiquitous Language: A common language developed collaboratively by developers and domain experts. This shared vocabulary ensures clear communication and consistent understanding across all stakeholders.
Entities and Value Objects:
Aggregates: Clusters of related entities and value objects treated as a single unit for data changes. Each aggregate has a root entity that ensures the integrity of the entire cluster.
Repositories: Mechanisms for retrieving and storing aggregates, providing a layer of abstraction over data access.
Services: Operations or processes that don't naturally fit within entities or value objects but are essential to the domain, such as processing a payment.
src/ports/repository.py from abc import ABC, abstractmethod from typing import List from src.entities import Entity class Repository(ABC): @abstractmethod def get(self, id: str) -> Entity: pass @abstractmethod def insert(self, entity: Entity) -> None: pass @abstractmethod def update(self, entity: Entity) -> None: pass
In this section, I do not provide a detailed example of implementing Domain-Driven Design (DDD) because it is a comprehensive methodology primarily focused on addressing complex business logic challenges. DDD excels at structuring and managing intricate business rules, but to fully realize its potential and address other coding concerns, it is best utilized within a complementary architectural framework. So, in the following section, Domain-Driven Design will be combined with Hexagonal Architecture to highlights it strengths and provide a solid foundation for solving additional coding problems beyond business logic, accompanied by a detailed example.
Domain-Driven Design (DDD) and Hexagonal Architecture complement each other by emphasizing clear boundaries and aligning software with business needs. DDD focuses on modeling the core domain and isolating business logic, while Hexagonal Architecture ensures this logic remains independent of external systems through ports and adapters. They address distinct but complementary concerns:
Hexagonal Architecture as the Framework:
Domain-Driven Design as the Core Logic:
Together, they enable scalable, testable, and flexible systems where the domain remains the central focus, insulated from changes in infrastructure or technology. This synergy ensures a robust design that adapts easily to evolving business requirements.
The following section offers a practical example of how Domain-Driven Design (DDD) and Hexagonal Architecture work together to create robust, maintainable, and adaptable software systems.
This project applies Hexagonal Architecture and Domain-Driven Design (DDD) to create scalable and maintainable systems, providing a modern and robust foundation for application development. Built with Python, it uses FastAPI as the web framework and DynamoDB as the database.
The project is organized as follows:
src/ports/repository.py from abc import ABC, abstractmethod from typing import List from src.entities import Entity class Repository(ABC): @abstractmethod def get(self, id: str) -> Entity: pass @abstractmethod def insert(self, entity: Entity) -> None: pass @abstractmethod def update(self, entity: Entity) -> None: pass
You can find the source code in my GitHub repository.
Incorporating Hexagonal Architecture and Domain-Driven Design (DDD) into Python applications fosters the development of systems that are maintainable, adaptable, and closely aligned with business objectives. Hexagonal Architecture ensures a clear separation between the core business logic and external systems, promoting flexibility and ease of testing. DDD emphasizes modeling the domain accurately, resulting in software that truly reflects business processes and rules. By integrating these methodologies, developers can create robust applications that not only meet current requirements but are also well-prepared to evolve with future business needs.
Connect me if you enjoyed this article!
The above is the detailed content of Building Maintainable Python Applications with Hexagonal Architecture and Domain-Driven Design. For more information, please follow other related articles on the PHP Chinese website!