When to compose
DDD CQRS ES
The Domain class in Protean acts as a composition root. It manages external
dependencies and injects them into objects during application startup.
Your domain should be composed at the start of the application lifecycle — once, before any request or task is handled. This means:
- Instantiate the
Domainand register elements (via decorators or manual registration). - Initialize the domain with
domain.init()to activate adapters, validate element registration, and resolve dependencies. - Push a domain context before processing requests, so that
current_domainis available throughout the call stack.
The exact integration point depends on your application framework.
FastAPI (recommended)
Protean provides built-in middleware for FastAPI that handles domain context management automatically:
from fastapi import FastAPI
from protean.integrations.fastapi import (
DomainContextMiddleware,
register_exception_handlers,
)
from my_app.domain import domain
# Initialize the domain at module load time
domain.init()
app = FastAPI()
# Middleware pushes/pops domain context per request
app.add_middleware(
DomainContextMiddleware,
route_domain_map={"/": domain},
)
# Map domain exceptions to HTTP status codes
register_exception_handlers(app)
The middleware ensures every request runs inside a domain context, and the
exception handlers translate ValidationError, ObjectNotFoundError, etc.
into appropriate HTTP responses.
See FastAPI Integration for the full guide.
Flask
For Flask, use before_request and after_request hooks to manage the
domain context:
import logging.config
from flask import Flask
from protean import Domain
from protean.domain.context import has_domain_context
from protean.fields import Integer, String
domain = Domain()
@domain.aggregate
class User:
first_name: String(max_length=50)
last_name: String(max_length=50)
age: Integer()
def create_app(config):
app = Flask(__name__, static_folder=None)
domain.config.from_object(config)
logging.config.dictConfig(domain.config["LOGGING_CONFIG"])
domain.init()
@app.before_request
def set_context():
if not has_domain_context():
# Push up a Domain Context
domain.domain_context().push()
@app.after_request
def pop_context(response):
# Pop the Domain Context
domain.domain_context().pop()
return response
return app
The domain is initialized once during create_app(), and the context is
pushed before each request and popped after.
Console applications and scripts
In simple console applications, compose the domain in main and use
a with block for the domain context:
from protean import Domain
from protean.fields import String
domain = Domain()
@domain.aggregate
class Task:
title: String(max_length=200, required=True)
if __name__ == "__main__":
domain.init()
with domain.domain_context():
repo = domain.repository_for(Task)
repo.add(Task(title="Write documentation"))
Background workers and the Protean server
The Protean server (protean server) handles domain composition internally.
You only need to point it at your domain module:
$ protean server --domain my_app.domain
The server initializes the domain, pushes a context, and manages the event processing loop. See Running the Server for details.
Key principles
- Compose once, early. Call
domain.init()at startup, not per-request. Initialization activates database connections, validates element registration, and is not designed to be called repeatedly. - Context per request. Each request or task should run inside its own
domain context (
domain.domain_context()). The context makescurrent_domainavailable and manages the Unit of Work lifecycle. - Let the framework manage it. Prefer framework-provided integration (FastAPI middleware, Flask hooks) over manual context management. This ensures contexts are properly cleaned up even when exceptions occur.