Skip to content

Hexagonal Architecture (Ports & Adapters) in Python SaaS

Hexagonal Architecture (Ports & Adapters) in Python SaaS

This diagram visualizes the structural separation of concerns in the python-saas template.

1. The Core Philosophy

The Hexagonal Architecture (also known as Ports and Adapters) ensures the Business Logic (Core) remains pure and isolated from external side effects like databases, APIs, or user interfaces.

In python-saas, this allows us to swap a PostgreSQL database for a NoSQL one, or change a REST API for a GraphQL one, without touching a single line of business logic.

2. Layer Definitions

The Core (Inner Circle)

  • Entities: Pure data structures (Pydantic models) representing the business domain (e.g., User, Project).
  • Use Cases: The orchestration logic. This is where the actual “features” live. Use cases only interact with Interfaces, never concrete implementations.
  • Interfaces (Ports): Abstract Base Classes (ABCs) that define the “contract” for what the core needs from the outside world (e.g., UserRepository).

The Infrastructure (Driven Adapters)

  • Persistence: Concrete implementations of the core interfaces (e.g., SQLAlchemyUserRepository).
  • External Adapters: Clients for email services, AI LLM providers, or cloud storage.
  • Configuration: How the system boots up, linking concrete adapters to abstract ports.

The UI Layer (Driving Adapters)

  • Web API: FastAPI routers that receive requests and trigger the relevant Use Case.
  • NiceGUI Interface: Reactive Python UI components that interact with the Core.

3. The Dependency Rule

Dependencies MUST always point inwards.

  • Infrastructure -> Core (Directly or via Ports)
  • UI -> Core
  • NEVER Core -> Infrastructure or Core -> UI.

4. Visual Flow

%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#3b82f6', 'edgeColor': '#ffffff', 'tertiaryColor': '#1f2937'}}}%%
graph TD
    subgraph UI_Layer [UI / Driving Adapters]
        Main[ui/main.py]
        API[api/routes.py]
    end

    subgraph Core_Layer [Core / Domain]
        UC[use_cases/]
        E[entities/]
        I[interfaces/ / Ports]
    end

    subgraph Infra_Layer [Infrastructure / Driven Adapters]
        DB[persistence/memory_repository.py]
        Ext[adapters/external_api.py]
    end

    Main --> UC
    API --> UC
    UC --> E
    UC --> I
    DB -.-> I
    Ext -.-> I

    style UI_Layer fill:#064e3b,stroke:#10b981,stroke-width:2px,color:#fff
    style Core_Layer fill:#1e3a8a,stroke:#3b82f6,stroke-width:4px,color:#fff
    style Infra_Layer fill:#78350f,stroke:#f59e0b,stroke-width:2px,color:#fff

    classDef default color:#fff,stroke:#fff

Key Principles

  1. Core Independence: The core/ folder contains no imports from ui/ or infrastructure/.
  2. Ports (Interfaces): Use abstract base classes in interfaces/ to define how the core interacts with the outside world.
  3. Adapters: Implementations in infrastructure/ (e.g., SQLAlchemy, Redis) satisfy the interfaces defined in the core.
  4. Testability: The core can be tested in 100% isolation by mocking the ports.