Skip to content

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:

  1. Instantiate the Domain and register elements (via decorators or manual registration).
  2. Initialize the domain with domain.init() to activate adapters, validate element registration, and resolve dependencies.
  3. Push a domain context before processing requests, so that current_domain is available throughout the call stack.

The exact integration point depends on your application framework.

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 makes current_domain available 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.