Skip to content

Concepts

ArchiPy organises every application into four strictly separated layers. Understanding these layers — and their import rules — is the foundation for writing maintainable, testable services.

The Four Layers

graph LR
    adapters -->|imports| helpers
    adapters -->|imports| models
    adapters -->|imports| configs
    helpers -->|imports| models
    helpers -->|imports| configs
    models -->|imports| configs
Layer Directory Responsibility
Configs configs/ Type-safe, environment-based configuration via pydantic_settings.BaseSettings
Models models/ Entities (SQLAlchemy), DTOs (Pydantic), Errors, Types — data structures only
Helpers helpers/ Pure utilities: decorators, interceptors, JWT, password, datetime
Adapters adapters/ External service integrations: databases, caches, queues, APIs

Warning: Imports flow inward only — configs ← models ← helpers ← adapters. Inner layers never import from outer layers. Violating this rule breaks testability and creates circular dependencies.


Layer Details

Configs

All configuration classes extend pydantic_settings.BaseSettings. Values are loaded from environment variables or .env files and validated at startup:

from archipy.configs.base_config import BaseConfig


class AppConfig(BaseConfig):
    """Application-specific configuration."""

    APP_NAME: str = "my-service"
    DEBUG: bool = False


config = AppConfig()
BaseConfig.set_global(config)

Models

The models layer contains data structures only — no I/O, no business logic.

  • Entities — SQLAlchemy domain model objects (BaseEntity)
  • DTOs — Pydantic BaseModel for data transfer across layer boundaries
  • Errors — Custom exception types extending BaseError
  • Types — Enumerations and type aliases

DTOs are divided into two groups:

Group Purpose Location Versioned?
Domain DTOs Cross the service boundary (client ↔ service) dtos/{domain}/domain/v{n}/ Yes
Repository DTOs Internal (logic ↔ repository ↔ adapter) dtos/{domain}/repository/ No

Helpers

The helpers layer contains pure utilities with no direct external I/O:

Sub-package Contents
helpers/utils/ JWT, password, TOTP, datetime, string, file, Prometheus
helpers/decorators/ @atomic, @ttl_cache, @retry, @singleton, @timeout, @timing
helpers/interceptors/ FastAPI rate limiting and metrics; gRPC tracing, metrics, exception handling
helpers/metaclasses/ SingletonMeta and other meta-programming utilities

Adapters

Adapters follow the Ports & Adapters (Hexagonal Architecture) pattern. Every adapter directory contains:

  • ports.py — abstract interface (ABC) describing what the adapter can do
  • adapters.py — concrete implementation against a real external service
  • mocks.py (where available) — in-memory test double for unit tests

Your business logic imports only from ports.py. This means you can swap the implementation (real adapter → mock) without changing any business code.


Architectural Flow

flowchart TD
    Client["Client (HTTP / gRPC / CLI)"]
    Container["configs/containers.py (DI wiring)"]
    Service["services/ (FastAPI endpoints)"]
    Logic["logics/ (Business rules + Unit of Work)"]
    Repository["repositories/ (Data access)"]
    DomainAdapter["adapters/ (Domain-specific, delegates to ArchiPy)"]
    ArchiPyAdapter["ArchiPy Adapters (DB, Cache, etc.)"]
    Models["models/ (Entities, DTOs, Errors)"]
    Configs["configs/app_config.py (BaseConfig)"]
    Client --> Service
    Container -->|injects| Service
    Service --> Logic
    Logic -->|may call| Logic
    Logic --> Repository
    Repository --> DomainAdapter
    DomainAdapter --> ArchiPyAdapter
    ArchiPyAdapter --> Models
    ArchiPyAdapter --> Configs
    Logic --> Models
    Service --> Models

Ports & Adapters in Practice

Production:  UserRepository → PostgresSQLAlchemyAdapter (real Postgres)
Test:        UserRepository → InMemorySQLAlchemyMock   (no Docker needed)

Your logic layer depends on the abstract port:

from archipy.adapters.redis.ports import RedisPort
from archipy.models.errors import NotFoundError


class UserSessionService:
    """Manages user sessions using a cache.

    Args:
        cache: Any implementation of RedisPort (real or mock).
    """

    def __init__(self, cache: RedisPort) -> None:
        self._cache = cache

    def get_session(self, session_id: str) -> str:
        """Retrieve a session by ID.

        Args:
            session_id: The session identifier.

        Returns:
            Serialised session data.

        Raises:
            NotFoundError: If the session does not exist.
        """
        value = self._cache.get(f"session:{session_id}")
        if value is None:
            raise NotFoundError(resource_type="session")
        return value

In tests, inject RedisMock instead of RedisAdapter — no Redis server needed.


Unit of Work

The logic layer is the unit of work boundary. Every public method on a logic class is decorated with @postgres_sqlalchemy_atomic_decorator, which opens a SQLAlchemy session, commits on success, and rolls back on any exception:

Service  →  Logic (@atomic)  →  Repository  →  Adapter  →  DB
                 ↑___________commit / rollback____________↑
from archipy.helpers.decorators.sqlalchemy_atomic import postgres_sqlalchemy_atomic_decorator
from models.dtos.user.domain.v1.user_dtos import UserRegistrationInputDTO, UserRegistrationOutputDTO


class UserRegistrationLogic:
    """Handles user registration within a single database transaction."""

    @postgres_sqlalchemy_atomic_decorator
    def register_user(self, input_dto: UserRegistrationInputDTO) -> UserRegistrationOutputDTO:
        """Validate uniqueness and create a new user.

        Args:
            input_dto: Registration data from the service layer.

        Returns:
            Output DTO for the newly created user.
        """
        ...

Note: Logic layer collaboration rules:

  • Logic classes may call other logic classes — nested @atomic calls reuse the open session.
  • Logic classes must never import or call a repository from another domain directly. > Cross-domain reads must go through the other domain's logic class.

    ```
    

    ✅ OrderCreationLogic → UserQueryLogic → UserRepository ❌ OrderCreationLogic → UserRepository (bypasses domain boundary) ```


Design Philosophy

ArchiPy provides standardised building blocks rather than enforcing a single architectural style. The same components work across:

  • Layered Architecture
  • Hexagonal Architecture (Ports & Adapters)
  • Clean Architecture
  • Domain-Driven Design
  • Service-Oriented Architecture

Teams maintain consistent practices while choosing the pattern that best fits their domain.


See Also