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
BaseModelfor 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 doadapters.py— concrete implementation against a real external servicemocks.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:
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
@atomiccalls 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¶
- Quickstart — five-minute "hello world" example
- Project Structure — recommended folder layout
- Configuration Management — loading from
.envfiles - Dependency Injection — wiring the full object graph
- Testing Strategy — unit and integration test patterns
- API Reference — full reference for all public classes