Skip to content

DDD Service โ€” ORM DB (Domain-Driven Design, hexagonal service with SQLAlchemy)

A Domain-Driven Design scaffold with a hexagonal (ports-and-adapters) layout. It keeps business logic isolated from I/O while allowing shared infrastructure when it is truly cross-cutting.

This template uses SQLAlchemy ORM for database operations, providing a unified abstraction layer that works with PostgreSQL, MySQL, SQLite, Oracle, MSSQL, and other SQLAlchemy-supported databases.

Examples: Usage with SQLAlchemy ยท External API Calls ยท Bank Balance Alert


Key Differences from Native DB

Aspect Native DB ORM DB (this template)
Database access Direct SQL via native libraries SQLAlchemy ORM models
Supported DBs Each DB needs its own handler Any SQLAlchemy-supported DB
Query style Raw SQL strings Python objects & methods
Schema Manual table creation Auto-generated from models
Learning curve Simpler for SQL experts Better for ORM users

Expected layout (after scaffold)

project/
  src/
    core/
      domain/
      infrastructure/
        database/
          base.py          # SQLAlchemy base, session manager
          models.py        # ORM models
          repository.py    # Generic SQLAlchemy repository
      application/
    modules/<feature>/{domain,application,infrastructure}
    utils/
    config/
    main.py
  tests/{unit,integration,performance}/
  container/
    scripts/
  assets/
  docs/
  .github/workflows/
  .vscode/
  .env
  pyproject.toml

Folder Descriptions

Folder Purpose Expected Content
src/ Main source code All Python modules, organized by DDD layers
src/core/ Shared cross-cutting code Domain entities, infrastructure adapters, and application services used across multiple features
src/core/domain/ Shared domain layer Base entities, value objects, shared domain services
src/core/application/ Shared application layer Factories, shared use-cases, cross-cutting orchestration
src/core/infrastructure/ Shared infrastructure Database handlers, HTTP clients, message brokers, external integrations
src/modules/<feature>/ Feature-specific code Bounded context with its own domain, application, and infrastructure layers
src/utils/ Utility functions Helpers, formatters, validators, decorators
src/config/ Configuration Settings, environment loaders, constants
tests/unit/ Unit tests Fast, isolated tests for domain logic and use-cases
tests/integration/ Integration tests Tests with real databases, APIs, or external services
tests/performance/ Performance tests Load tests, benchmarks, stress tests
container/ Container configuration Dockerfile, docker-compose.yaml, container scripts
container/scripts/ Container scripts Entrypoint scripts, health checks, init scripts
assets/ Static resources Images, icons, fonts, static files
data/ Data storage SQLite databases, CSV files, XLSX files, JSON data, seed data
docs/ Documentation API docs, architecture diagrams, user guides
.github/workflows/ CI/CD pipelines GitHub Actions workflows for tests, linting, deployment
.vscode/ Editor settings VS Code workspace settings, extensions, launch configs

Layers

Domain (core/domain or modules/<feature>/domain)

What goes here: Entities, value objects, domain services (pure business logic), and the ports (interfaces) the domain needs. No framework or I/O.

# entities.py
from dataclasses import dataclass
from datetime import datetime

@dataclass
class Note:
    id: str
    title: str
    created_at: datetime
# ports.py
from abc import ABC, abstractmethod
from typing import Iterable
from .entities import Note

class NoteRepository(ABC):
    @abstractmethod
    def add(self, note: Note) -> Note: ...
    @abstractmethod
    def get(self, note_id: str) -> Note | None: ...
    @abstractmethod
    def list(self) -> Iterable[Note]: ...

Application (core/application or modules/<feature>/application)

What goes here: Use-case orchestration; coordinates domain objects and ports. Enforces transaction boundaries and policies; still framework-free.

# use_cases.py
from datetime import datetime
import uuid
from ..domain.entities import Note
from ..domain.ports import NoteRepository

class CreateNote:
    def __init__(self, repo: NoteRepository):
        self.repo = repo

    def execute(self, title: str) -> Note:
        note = Note(id=uuid.uuid4().hex, title=title, created_at=datetime.utcnow())
        return self.repo.add(note)

Infrastructure (core/infrastructure or modules/<feature>/infrastructure)

What goes here: Adapters implementing ports (DB, HTTP clients, brokers), configuration glue, persistence mappers. Keep side effects here.

# repositories.py
from ..domain.entities import Note
from ..domain.ports import NoteRepository

class InMemoryNoteRepository(NoteRepository):
    def __init__(self):
        self._items: dict[str, Note] = {}

    def add(self, note: Note) -> Note:
        self._items[note.id] = note
        return note

Shared database backends live under core/infrastructure/database/, with runtime selection via DB_BACKEND env var (json, csv, sqlite, postgresql, mariadb, mysql, mssql, oracle).


Modules (modules/<feature>)

What goes here: Feature/bounded-context composition โ€” wire domain + app + infra for that feature. Also entrypoints like API/CLI handlers.


Rules of thumb

Layer Responsibility
Domain Pure logic and contracts; no I/O or frameworks
Application Orchestrate use-cases, transactions, and policies; still framework-free
Infrastructure All I/O adapters implementing ports (DB, HTTP, queues, files)
Modules Group everything per feature/context and provide entrypoints/wiring

Keep core/ only for truly shared cross-cutting pieces.


Learn more


๐Ÿ“š Acknowledgments

This skeleton is inspired by the foundational work of Eric Evans in his seminal book Domain-Driven Design: Tackling Complexity in the Heart of Software. If you're new to DDD or want to deepen your understanding of the patterns used here, we highly recommend reading it.

๐Ÿ“– Domain-Driven Design: Tackling Complexity in the Heart of Software โ€” Eric Evans