Hexagonal Architecture with FastAPI for Enterprise Apps

In the rapidly evolving landscape of software development, building applications that are robust, maintainable, and adaptable to change is paramount. Enterprise applications, in particular, demand architectures that can withstand shifting business requirements and technology trends. This is where Hexagonal Architecture, also known as Ports and Adapters, shines. When paired with a modern, high-performance web framework like FastAPI, it offers a powerful approach to constructing resilient systems.

This article will guide you through the principles of Hexagonal Architecture and demonstrate a practical implementation using FastAPI, complete with real-world enterprise business application examples. We’ll explore how to structure your project to achieve clear separation of concerns, enhance testability, and ensure your core business logic remains independent of external frameworks or databases.

Understanding Hexagonal Architecture

Hexagonal Architecture, introduced by Alistair Cockburn, is a design pattern that aims to create highly decoupled application components. Its core idea is to isolate the application’s core business logic from external concerns such as user interfaces, databases, or third-party APIs. This isolation makes the application more flexible, testable, and maintainable.

What is Hexagonal Architecture?

Imagine your application’s core as a hexagon. The business logic resides safely inside this hexagon, oblivious to the outside world. All communication with external components happens through well-defined ‘ports’ on the hexagon’s edges. ‘Adapters’ then plug into these ports, translating external technologies into a format the core understands, and vice-versa.

Hexagonal Architecture ensures that the application can be driven by different kinds of clients (users, automated tests, batch scripts) and can use different kinds of databases or other external systems without being rewritten. The core business logic remains pristine and untouched.

Core Concepts: Ports and Adapters

The entire philosophy revolves around two key concepts:

  • Ports: These are interfaces that define how the core application interacts with the outside world. They represent contracts. Think of them as the ‘sockets’ on the hexagon.
    • Driving Ports (Inbound): Used by external actors (e.g., a web UI, a message queue) to interact with the application. They define the operations the application’s core offers.
    • Driven Ports (Outbound): Used by the application’s core to interact with external systems (e.g., a database, an external API). They define the operations the external system must provide.
  • Adapters: These are concrete implementations that plug into the ports. They translate specific technologies or protocols into the generic interface defined by a port. Think of them as the ‘plugs’ that fit into the sockets.
    • Driving Adapters: Implement a driving port. Examples include a FastAPI controller exposing a REST API, a CLI tool, or a consumer for a message queue. They invoke the application’s core logic through its driving port.
    • Driven Adapters: Implement a driven port. Examples include a SQLAlchemy repository for a database, a client for a third-party API, or a file system writer. They provide the necessary infrastructure for the core to perform its operations.

The crucial point is that the core business logic only knows about the ports (interfaces), not the concrete adapters. This inversion of control is what gives Hexagonal Architecture its power.

A clean, professional illustration depicting a central hexagonal shape representing the application core, surrounded by various external components like a database, a user interface, and an external API. Lines connect the hexagon to these components, labeled as 'ports' and 'adapters', illustrating the concept of decoupled architecture.

Why Hexagonal Architecture for Enterprise Apps?

For enterprise applications, the benefits of this architectural style are significant:

  • Maintainability: Changes to external technologies (e.g., switching databases) don’t require changes to the core business logic. Only the relevant adapter needs modification.
  • Testability: The core business logic can be tested in isolation, without needing a running database, web server, or external services. You can easily mock or stub out adapters.
  • Flexibility: The application can be exposed through multiple interfaces (e.g., a REST API, a GraphQL API, a command-line interface) by simply adding new driving adapters.
  • Technology Independence: The core is not coupled to any specific framework, database, or UI library, making it easier to adopt new technologies or swap existing ones.
  • Scalability: Clear separation of concerns often leads to more modular components that can be scaled independently if needed.

FastAPI: The Perfect Partner

FastAPI is a modern, fast (high-performance) web framework for building APIs with Python 3.7+ based on standard Python type hints. Its features align exceptionally well with the principles of Hexagonal Architecture.

Why FastAPI?

FastAPI offers several compelling advantages:

  • Performance: Built on Starlette for the web parts and Pydantic for data validation and serialization, FastAPI is incredibly fast.
  • Developer Experience: Automatic interactive API documentation (Swagger UI and ReDoc) makes development and consumption a breeze.
  • Asynchronous Support: Native async/await support is crucial for building high-concurrency applications.
  • Pydantic: Provides robust data validation, serialization, and deserialization, which is perfect for defining domain models and DTOs.
  • Dependency Injection System: FastAPI’s powerful dependency injection system is a game-changer for Hexagonal Architecture. It allows you to inject ports (interfaces) into your API endpoints, with adapters providing the concrete implementations.

Fitting FastAPI into Hexagonal Architecture

In a Hexagonal Architecture, FastAPI primarily acts as a driving adapter. It takes external HTTP requests, validates them, and then invokes the appropriate operation on a driving port (an application service interface). The application core, which contains the business logic, remains completely unaware that it’s being driven by a web API.

Furthermore, FastAPI’s dependency injection system is invaluable for wiring up the application. You can define abstract base classes for your ports and then register concrete adapter implementations (e.g., a database repository) that FastAPI will inject into your route handlers or application services.

Designing Your Hexagonal Application Structure

A clear project structure is vital for implementing Hexagonal Architecture effectively. Here’s a common layout for a Python project:

Project Layout

. 
├── src/
│   ├── core/                  # The application core (the hexagon)
│   │   ├── domain/            # Entities, Value Objects, Domain Services
│   │   └── application/       # Driving Ports (Application Services)
│   │       └── ports/         # Interfaces defining business operations
│   │       └── services/      # Implementations of driving ports (optional, can be in domain)
│   ├── infrastructure/        # Adapters (outside the hexagon)
│   │   ├── persistence/       # Driven Adapters (e.g., database repositories)
│   │   │   └── adapters/      # Concrete implementations for database access
│   │   │   └── models/        # ORM models (if different from domain models)
│   │   ├── web/               # Driving Adapters (e.g., FastAPI controllers)
│   │   │   └── adapters/      # FastAPI routes and DTOs
│   │   │   └── routers/       # API route definitions
│   │   ├── messaging/         # Other driven/driving adapters (e.g., message queues)
│   │   └── __init__.py
│   ├── config.py              # Configuration settings
│   └── main.py                # FastAPI application entry point, dependency wiring
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── requirements.txt
├── Dockerfile
└── README.md

Defining the Core Domain

The core/domain directory holds your pure business logic. This is where your entities, value objects, and domain services reside. They should be POPOs (Plain Old Python Objects) with no dependencies on frameworks or infrastructure components.

# src/core/domain/entities.py

from dataclasses import dataclass, field
from datetime import datetime
import uuid

@dataclass
class Product:
    product_id: uuid.UUID
    name: str
    description: str
    price: float
    stock: int

@dataclass
class OrderItem:
    product: Product
    quantity: int

    @property
    def total_item_price(self) -> float:
        return self.product.price * self.quantity

@dataclass
class Order:
    order_id: uuid.UUID = field(default_factory=uuid.uuid4)
    customer_id: uuid.UUID
    items: list[OrderItem]
    order_date: datetime = field(default_factory=datetime.utcnow)
    status: str = "pending"

    @property
    def total_amount(self) -> float:
        return sum(item.total_item_price for item in self.items)

    def approve(self):
        if self.status == "pending":
            self.status = "approved"
            # Add more business logic here, e.g., trigger inventory deduction
        else:
            raise ValueError("Order cannot be approved from current status.")

# src/core/domain/exceptions.py
class ProductNotFoundError(Exception):
    pass

class InsufficientStockError(Exception):
    pass

class OrderNotFoundError(Exception):
    pass

Implementing Ports

Ports are Python abstract base classes (ABCs) that define the contracts for interacting with your core domain. They don’t contain any implementation details.

# src/core/application/ports/order_repository.py

from abc import ABC, abstractmethod
from typing import Optional
from uuid import UUID
from core.domain.entities import Order, Product

class OrderRepository(ABC):
    """Driven Port: Defines the interface for persisting and retrieving orders."""

    @abstractmethod
    async def get_order_by_id(self, order_id: UUID) -> Optional[Order]:
        pass

    @abstractmethod
    async def save_order(self, order: Order) -> None:
        pass

    @abstractmethod
    async def get_product_by_id(self, product_id: UUID) -> Optional[Product]:
        pass

# src/core/application/ports/order_service.py

from abc import ABC, abstractmethod
from typing import List
from uuid import UUID
from core.domain.entities import Order

# DTOs for input/output, often defined alongside ports or in a separate DTO module
class CreateOrderCommand:
    customer_id: UUID
    product_ids_with_quantities: dict[UUID, int]

class OrderDTO:
    order_id: UUID
    customer_id: UUID
    total_amount: float
    status: str
    order_date: datetime
    items: List[dict]

class OrderService(ABC):
    """Driving Port: Defines the interface for application-level order operations."""

    @abstractmethod
    async def create_order(self, command: CreateOrderCommand) -> OrderDTO:
        pass

    @abstractmethod
    async def get_order_details(self, order_id: UUID) -> Optional[OrderDTO]:
        pass

    @abstractmethod
    async def approve_order(self, order_id: UUID) -> OrderDTO:
        pass

Creating Adapters

Adapters provide the concrete implementations for your ports. They bridge the gap between your core logic and external technologies.

A conceptual diagram showing a central hexagonal core with 'Order Service' and 'Order Repository' ports. Two external adapters are shown connecting to these ports: one labeled 'FastAPI Web Adapter' with an arrow pointing into a 'Driving Port', and another labeled 'SQLAlchemy Persistence Adapter' with an arrow pointing out from a 'Driven Port'.

Practical Enterprise Example: An Order Management System

Let’s illustrate with an Order Management System. We’ll focus on creating an order and fetching its details.

Domain Model: Order and Product

We’ve already defined our Product and Order entities in src/core/domain/entities.py. These are pure Python objects representing our business concepts.

Defining Application Services (Ports)

The OrderService ABC (src/core/application/ports/order_service.py) defines our driving port. Now, let’s implement the actual application service that uses the OrderRepository (a driven port).

# src/core/application/services/order_application_service.py

from typing import Optional
from uuid import UUID

from core.application.ports.order_repository import OrderRepository
from core.application.ports.order_service import OrderService, CreateOrderCommand, OrderDTO
from core.domain.entities import Order, OrderItem, Product
from core.domain.exceptions import ProductNotFoundError, InsufficientStockError, OrderNotFoundError

class OrderApplicationService(OrderService):
    """Implementation of the OrderService driving port."""

    def __init__(self, order_repository: OrderRepository):
        self.order_repository = order_repository

    async def create_order(self, command: CreateOrderCommand) -> OrderDTO:
        order_items: list[OrderItem] = []
        for product_id, quantity in command.product_ids_with_quantities.items():
            product = await self.order_repository.get_product_by_id(product_id)
            if not product:
                raise ProductNotFoundError(f"Product with ID {product_id} not found.")
            if product.stock < quantity:
                raise InsufficientStockError(f"Insufficient stock for product {product.name}.")
            order_items.append(OrderItem(product=product, quantity=quantity))
            # In a real scenario, you'd decrement stock here or via a separate domain event

        new_order = Order(customer_id=command.customer_id, items=order_items)
        await self.order_repository.save_order(new_order)
        return self._to_order_dto(new_order)

    async def get_order_details(self, order_id: UUID) -> Optional[OrderDTO]:
        order = await self.order_repository.get_order_by_id(order_id)
        if not order:
            return None
        return self._to_order_dto(order)

    async def approve_order(self, order_id: UUID) -> OrderDTO:
        order = await self.order_repository.get_order_by_id(order_id)
        if not order:
            raise OrderNotFoundError(f"Order with ID {order_id} not found.")
        
        order.approve() # Business logic resides in the domain entity
        await self.order_repository.save_order(order)
        return self._to_order_dto(order)

    def _to_order_dto(self, order: Order) -> OrderDTO:
        """Helper to convert domain Order entity to a DTO."""
        return OrderDTO(
            order_id=order.order_id,
            customer_id=order.customer_id,
            total_amount=order.total_amount,
            status=order.status,
            order_date=order.order_date,
            items=[
                {"product_id": item.product.product_id, "name": item.product.name, "quantity": item.quantity, "price": item.product.price}
                for item in order.items
            ]
        )

Implementing Infrastructure (Adapters)

Database Adapter (e.g., SQLAlchemy)

This adapter implements the OrderRepository driven port, using SQLAlchemy to interact with a database.

# src/infrastructure/persistence/adapters/sqlalchemy_order_repository.py

from typing import Optional
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload

from core.application.ports.order_repository import OrderRepository
from core.domain.entities import Order, Product, OrderItem
from infrastructure.persistence.models.sqlalchemy_models import OrderModel, ProductModel, OrderItemModel # Assuming these are defined elsewhere

class SQLAlchemyOrderRepository(OrderRepository):
    """Driven Adapter: SQLAlchemy implementation of OrderRepository."""

    def __init__(self, session: AsyncSession):
        self.session = session

    async def get_order_by_id(self, order_id: UUID) -> Optional[Order]:
        stmt = select(OrderModel).where(OrderModel.id == order_id).options(selectinload(OrderModel.items).selectinload(OrderItemModel.product))
        result = await self.session.execute(stmt)
        order_model = result.scalars().first()
        if order_model:
            return self._to_domain_order(order_model)
        return None

    async def save_order(self, order: Order) -> None:
        order_model = self._to_sqlalchemy_order_model(order)
        self.session.add(order_model)
        await self.session.flush() # Flush to get ID if new, but not commit

    async def get_product_by_id(self, product_id: UUID) -> Optional[Product]:
        stmt = select(ProductModel).where(ProductModel.id == product_id)
        result = await self.session.execute(stmt)
        product_model = result.scalars().first()
        if product_model:
            return self._to_domain_product(product_model)
        return None

    def _to_domain_order(self, model: OrderModel) -> Order:
        items = [
            OrderItem(
                product=self._to_domain_product(item_model.product),
                quantity=item_model.quantity
            )
            for item_model in model.items
        ]
        return Order(
            order_id=model.id,
            customer_id=model.customer_id,
            items=items,
            order_date=model.order_date,
            status=model.status
        )

    def _to_sqlalchemy_order_model(self, order: Order) -> OrderModel:
        order_model = OrderModel(
            id=order.order_id,
            customer_id=order.customer_id,
            order_date=order.order_date,
            status=order.status
        )
        for item in order.items:
            order_model.items.append(OrderItemModel(
                product_id=item.product.product_id,
                quantity=item.quantity
            ))
        return order_model

    def _to_domain_product(self, model: ProductModel) -> Product:
        return Product(
            product_id=model.id,
            name=model.name,
            description=model.description,
            price=model.price,
            stock=model.stock
        )

# NOTE: sqlalchemy_models.py (simplified for brevity, define your actual ORM models)
# from sqlalchemy import Column, String, Float, Integer, DateTime, ForeignKey
# from sqlalchemy.dialects.postgresql import UUID
# from sqlalchemy.orm import relationship, declarative_base
# import datetime
# import uuid

# Base = declarative_base()

# class ProductModel(Base):
#     __tablename__ = "products"
#     id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
#     name = Column(String, nullable=False)
#     description = Column(String)
#     price = Column(Float, nullable=False)
#     stock = Column(Integer, nullable=False)

# class OrderModel(Base):
#     __tablename__ = "orders"
#     id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
#     customer_id = Column(UUID(as_uuid=True), nullable=False)
#     order_date = Column(DateTime, default=datetime.datetime.utcnow)
#     status = Column(String, default="pending")
#     items = relationship("OrderItemModel", back_populates="order", cascade="all, delete-orphan")

# class OrderItemModel(Base):
#     __tablename__ = "order_items"
#     id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
#     order_id = Column(UUID(as_uuid=True), ForeignKey("orders.id"), nullable=False)
#     product_id = Column(UUID(as_uuid=True), ForeignKey("products.id"), nullable=False)
#     quantity = Column(Integer, nullable=False)
#     order = relationship("OrderModel", back_populates="items")
#     product = relationship("ProductModel")

FastAPI REST Adapter

This adapter defines your API endpoints and uses FastAPI’s dependency injection to get an instance of OrderService.

# src/infrastructure/web/routers/order_router.py

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from typing import List
from uuid import UUID

from core.application.ports.order_service import OrderService, CreateOrderCommand, OrderDTO
from core.domain.exceptions import ProductNotFoundError, InsufficientStockError, OrderNotFoundError

router = APIRouter()

# DTOs for FastAPI request/response bodies
class CreateOrderRequest(BaseModel):
    customer_id: UUID
    product_ids_with_quantities: dict[UUID, int]

class OrderResponse(BaseModel):
    order_id: UUID
    customer_id: UUID
    total_amount: float
    status: str
    order_date: datetime
    items: List[dict]

# Dependency for injecting the OrderService
# This will be overridden in main.py to provide the concrete implementation
async def get_order_service() -> OrderService:
    raise NotImplementedError("Dependency not implemented")

@router.post("/orders", response_model=OrderResponse, status_code=status.HTTP_201_CREATED)
async def create_order_endpoint(
    request: CreateOrderRequest,
    order_service: OrderService = Depends(get_order_service)
):
    try:
        command = CreateOrderCommand(
            customer_id=request.customer_id,
            product_ids_with_quantities=request.product_ids_with_quantities
        )
        order_dto = await order_service.create_order(command)
        return OrderResponse(**order_dto.dict()) # Convert DTO to Pydantic response model
    except (ProductNotFoundError, InsufficientStockError) as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))

@router.get("/orders/{order_id}", response_model=OrderResponse)
async def get_order_endpoint(
    order_id: UUID,
    order_service: OrderService = Depends(get_order_service)
):
    order_dto = await order_service.get_order_details(order_id)
    if not order_dto:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found")
    return OrderResponse(**order_dto.dict())

@router.post("/orders/{order_id}/approve", response_model=OrderResponse)
async def approve_order_endpoint(
    order_id: UUID,
    order_service: OrderService = Depends(get_order_service)
):
    try:
        order_dto = await order_service.approve_order(order_id)
        return OrderResponse(**order_dto.dict())
    except OrderNotFoundError as e:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
    except ValueError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))

Finally, in your main.py, you would wire everything together:

# src/main.py

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

from core.application.ports.order_repository import OrderRepository
from core.application.ports.order_service import OrderService
from core.application.services.order_application_service import OrderApplicationService
from infrastructure.persistence.adapters.sqlalchemy_order_repository import SQLAlchemyOrderRepository
from infrastructure.persistence.models.sqlalchemy_models import Base # Import your Base for metadata
from infrastructure.web.routers import order_router

# Database setup (replace with your actual database URL)
DATABASE_URL = "sqlite+aiosqlite:///./test.db"
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)

async def init_db():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

app = FastAPI(title="Hexagonal FastAPI Order System")

@app.on_event("startup")
async def on_startup():
    await init_db()

# Dependency to get a database session
async def get_db_session():
    async with AsyncSessionLocal() as session:
        yield session

# Dependency for OrderRepository (Driven Adapter)
async def get_order_repository(session: AsyncSession = Depends(get_db_session)) -> OrderRepository:
    return SQLAlchemyOrderRepository(session)

# Dependency for OrderService (Driving Port implementation)
async def get_actual_order_service(repository: OrderRepository = Depends(get_order_repository)) -> OrderService:
    return OrderApplicationService(repository)

# Override the default dependency in the router
order_router.router.dependency_overrides[order_router.get_order_service] = get_actual_order_service

app.include_router(order_router.router, prefix="/api/v1")

# Example of adding initial data (for testing purposes)
@app.on_event("startup")
async def seed_data():
    async with AsyncSessionLocal() as session:
        # Add a dummy product for creating orders
        existing_product = await session.execute(select(ProductModel).where(ProductModel.name == "Laptop X"))
        if not existing_product.scalars().first():
            product_id = uuid.uuid4()
            session.add(ProductModel(id=product_id, name="Laptop X", description="High-performance laptop", price=1200.00, stock=50))
            await session.commit()
            print(f"Seeded Product ID: {product_id}")

Benefits and Trade-offs

Implementing Hexagonal Architecture with FastAPI provides numerous advantages, but it’s important to acknowledge potential trade-offs.

Key Advantages

  • Decoupled Design: The core business logic is completely independent of the web framework, database, or any other external technology. This makes the system extremely resilient to changes.
  • Enhanced Testability: Unit testing the domain and application services becomes trivial. You can easily mock the repository and other driven ports without needing to set up a full database or web server.
  • Improved Maintainability: When a new feature is required, or a bug needs fixing, developers can quickly identify the relevant components without wading through intertwined layers.
  • Flexibility and Adaptability: Switching databases (e.g., from SQL to NoSQL), changing messaging systems, or even exposing a new API (e.g., GraphQL alongside REST) involves creating new adapters rather than refactoring the core.
  • Clearer Responsibilities: Each layer has a distinct responsibility, leading to cleaner code and easier onboarding for new team members.

A vibrant illustration of software components neatly separated into layers. A central 'Domain Core' is surrounded by 'Application Services', which are then connected to 'Web Adapters' and 'Database Adapters' via clearly defined interfaces, showcasing modularity and clear separation of concerns.

Potential Challenges

  • Initial Learning Curve: For teams unfamiliar with the pattern, there’s an initial overhead in understanding ports, adapters, and the dependency inversion principle.
  • Increased Boilerplate: Defining interfaces (ports) and separate implementations (adapters) can lead to more files and lines of code compared to a simpler layered architecture, especially for smaller projects.
  • Complexity for Simple Applications: For a very small, CRUD-heavy application with stable requirements, the added architectural overhead might not be justified.
  • Dependency Management: While FastAPI’s dependency injection helps, managing the wiring of all components can still be a complex task in very large applications.

Conclusion

Hexagonal Architecture, when combined with the power and developer-friendliness of FastAPI, offers a robust and scalable solution for building complex enterprise applications. By strictly separating your core business logic from external concerns through ports and adapters, you gain unparalleled flexibility, testability, and maintainability. While there’s an initial investment in understanding and structuring your project, the long-term benefits in terms of adaptability and resilience far outweigh the challenges. Embracing this architectural style empowers development teams in the US and globally to build systems that are not just functional, but truly future-proof.

Frequently Asked Questions

What’s the main difference between Hexagonal Architecture and traditional Layered Architecture?

The primary difference lies in the direction of dependencies. In traditional layered architecture, dependencies typically flow downwards (e.g., UI depends on Service, Service depends on Repository). This can lead to the core business logic being coupled to infrastructure concerns. Hexagonal Architecture, through ports and adapters and the Dependency Inversion Principle, ensures that the core domain is independent, with dependencies pointing inwards towards the core. This makes the core truly isolated and testable.

Is Hexagonal Architecture suitable for all types of applications?

While highly beneficial, Hexagonal Architecture might introduce unnecessary complexity for very simple CRUD applications or prototypes with stable requirements. It truly shines in complex enterprise applications where maintainability, testability, and the ability to swap external technologies are critical. For applications with rich domain models and evolving business rules, the investment in this architecture pays off significantly.

How does FastAPI’s dependency injection system help with Hexagonal Architecture?

FastAPI’s dependency injection is crucial for wiring up the different components. You define your ports as abstract classes (interfaces) and then register the concrete adapter implementations with FastAPI’s dependency system. When an endpoint requests a port, FastAPI automatically provides the corresponding adapter implementation. This allows the FastAPI web adapter to depend on the application service port, and the application service to depend on the repository port, without knowing their concrete implementations, thus upholding the dependency inversion principle.

Can I use other frameworks or databases with Hexagonal Architecture in Python?

Absolutely! That’s one of the core strengths of Hexagonal Architecture. Since your core domain and application services only depend on abstract ports, you can swap out any external technology by simply implementing a new adapter for that technology. For example, you could replace SQLAlchemy with MongoEngine by creating a MongoOrderRepository that implements the same OrderRepository port, without touching your business logic or even your FastAPI endpoints (beyond potentially updating the dependency injection configuration).

Leave a Reply

Your email address will not be published. Required fields are marked *