Skip to content

Configuration

Applies to: DDD · CQRS · Event Sourcing

Protean's configuration is managed through the Domain object, which can be configured in multiple ways:

Direct Configuration

Pass a configuration dictionary when initializing the Domain object:

domain = Domain(config={'debug': True, 'testing': True})

Configuration Files

Place configuration can be supplied in a TOML file in your project directory or up to two levels of parent directories.

Protean searches for configuration files in the following order:

  1. .domain.toml
  2. domain.toml
  3. pyproject.toml (under the [tool.protean] section)

Generating a new configuration file

When initializing a new Protean application using the new command, a domain.toml configuration file is automatically generated with sensible defaults.

A sample configuration file is below:

debug = true
testing = true
secret_key = "tvTpk3PAfkGr5x9!2sFU%XpW7bR8cwKA"
identity_strategy = "uuid"
identity_type = "string"
event_processing = "sync"
command_processing = "sync"

[databases.default]
provider = "memory"

[databases.memory]
provider = "memory"

[brokers.default]
provider = "inline"

[caches.default]
provider = "memory"

[event_store]
provider = "memory"

[custom]
foo = "bar"

[staging]
event_processing = "async"
command_processing = "sync"

[staging.databases.default]
provider = "sqlite"
database_url = "sqlite:///test.db"

[staging.brokers.default]
provider = "redis_pubsub"
URI = "redis://staging.example.com:6379/2"
TTL = 300

[staging.custom]
foo = "qux"

[prod]
event_processing = "async"
command_processing = "async"

[prod.databases.default]
provider = "postgresql"
database_url = "postgresql://postgres:postgres@localhost:5432/postgres"

[prod.brokers.default]
provider = "redis_pubsub"
URI = "redis://prod.example.com:6379/2"
TTL = 30

[prod.event_store]
provider = "message_db"
database_uri = "postgresql://message_store@localhost:5433/message_store"

[prod.custom]
foo = "quux"

[prod.telemetry]
enabled = true
service_name = "my-service"
exporter = "otlp"
endpoint = "http://otel-collector:4317"

Basic Configuration Parameters

debug

Specifies if the application is running in debug mode.

Do not enable debug mode when deploying in production.

Default: False

testing

Enable testing mode. Exceptions are propagated rather than handled by the domain’s error handlers. Extensions may also change their behavior to facilitate easier testing. You should enable this in your own tests.

Default: False

secret_key

A secret key that will be used for security related needs by extensions or your application. It should be a long random bytes or str.

You can generate a secret key with the following command:

> python -c 'import secrets; print(secrets.token_hex())'
c4bf0121035265bf44657217c33a7d041fe9e505961fc7da5d976aa0eaf5cf94

Do not reveal your secret key when posting questions or committing code.

env

Reserved for informational use. Protean does not read this key to select which environment overlay ([staging], [prod], etc.) to apply: overlay selection is driven entirely by the PROTEAN_ENV environment variable, described in Multiple Environments. Setting env in domain.toml has no effect on Protean's own behavior.

Default: None

identity_strategy

The default strategy to use to generate an identity value. Can be overridden at the Auto field level.

Supported options are uuid and function.

If the identity_strategy is chosen to be a function, identity_function has to be mandatorily specified during domain object initialization.

Default: uuid

identity_type

The type of the identity value. Can be overridden at the Auto field level.

Supported options are integer, string, and uuid.

Default: string

command_processing

Whether to process commands synchronously or asynchronously.

Supported options are sync and async.

Default: async

Note

The protean new scaffold writes an explicit command_processing = "sync" override into the generated domain.toml (see the sample above), which is why new projects behave synchronously by default. The framework default, absent any override, is async.

command_default_timeout

Default validity window, in seconds, applied to every command when no explicit deadline/timeout is passed to domain.process() and the handling command handler declares no timeout option. When the window elapses before a handler runs, the command is rejected (sync) or skipped without retry (async). See Deadlines and Timeouts.

None disables the default: commands never expire unless a deadline is requested explicitly or through a handler's timeout option.

Default: None

event_processing

Whether to process events synchronously or asynchronously.

Supported options are sync and async.

Default: async

Note

The protean new scaffold writes an explicit event_processing = "sync" override into the generated domain.toml (see the sample above), which is why new projects behave synchronously by default. The framework default, absent any override, is async.

message_processing

Whether to process incoming messages from external brokers (subscribers) synchronously or asynchronously.

Supported options are sync and async.

Default: async

source_uri

Overrides the CloudEvents source URI-reference attribute stamped on outgoing event/command envelopes. When unset, Protean derives urn:protean:<domain-name> from the domain's name.

Default: None

lenient_deserialization

When true, deserializing a stored event or command payload drops fields that no longer exist on the current class (recording their names under metadata.extensions["_dropped_fields"]) instead of raising a DeserializationError. Useful when reading legacy payloads written before a field was removed, without an upcaster.

The dropped-field record is read-time observability on the deserialized message: it is not written back to the event store and, being _-prefixed, is not emitted as a CloudEvents extension. Dropping only removes unknown fields, a payload still missing a required field raises as usual.

The default is strict: an unknown field raises, so a typo or genuine schema drift is not silently swallowed. A per-event lenient meta option (@domain.event(lenient=True/False)) overrides this config for a specific class. Field-rename renamed_from aliases resolve first, so a renamed old key is kept, not dropped.

Default: false

snapshot_threshold

The threshold number of aggregate events after which a snapshot is created to optimize performance.

Applies only when aggregates are event sourced.

Default: 10

Adapter Configuration

databases

Database repositories are used to access the underlying data store for persisting and retrieving domain objects.

They are defined in the [databases] section.

[databases.default]
provider = "memory"

[databases.memory]
provider = "memory"

[databases.sqlite]
provider = "sqlite"
database_uri = "sqlite:///test.db"

You can define as many databases as you need. The default database is identified by the default key, and is used when you do not specify a database name when accessing the domain.

The only other database defined by default is memory, which is the in-memory stub database provider.

The persistence store defined here is then specified in the provider key of aggregates and entities to assign them a specific database.

@domain.aggregate(provider="sqlite")  # (1)
class User:
    name: String(max_length=50)
    email: String(max_length=254)
  1. sqlite is the key of the database definition in the [databases.sqlite] section.

SQLAlchemy providers (postgresql, mssql) accept pool tuning keys directly on the [databases.<name>] block:

Key Default Purpose
pool_size 5 Base connections kept open per worker
max_overflow 10 Additional temporary connections beyond pool_size
pool_recycle unset Recycle connections older than N seconds

Setting pool_size below 5 triggers a LOW_POOL_SIZE warning from protean check (suppressed when PROTEAN_ENV is development or testing).

Read more in Adapters → Database or the full catalogue in Server Hardening reference.

caches

This section holds definitions for cache infrastructure.

[caches.default]
provider = "memory"

[caches.redis]
provider = "redis"
URI = "redis://127.0.0.1:6379/2"
TTL = 300
max_connections = 50
socket_timeout = 5
socket_connect_timeout = 5
retry_on_timeout = true

Default provider: memory

Redis caches forward max_connections, socket_timeout, socket_connect_timeout, and retry_on_timeout to the underlying redis.ConnectionPool.

Read more in Adapters → Cache section.

broker

This section holds configurations for message brokers.

[brokers.default]
provider = "memory"

[brokers.redis]
provider = "redis_pubsub"
URI = "redis://127.0.0.1:6379/0"
IS_ASYNC = true
max_connections = 50
socket_timeout = 5
socket_connect_timeout = 5
retry_on_timeout = true

Default provider: memory

Redis brokers (redis_pubsub, redis_stream) forward the same pool keys as the Redis cache to redis.ConnectionPool.

Read more in Adapters → Broker section.

event_store

The event store that stores event and command messages is defined in this section.

[event_store]
provider = "message_db"
database_uri = "postgresql://message_store@localhost:5433/message_store"
max_connections = 20

Note that there can only be only event store defined per domain.

Default provider: memory

MessageDB forwards max_connections through conn_info to cap the connection pool used for event reads and writes.

Read more in Adapters → Event Store section.

server

This section configures the Protean server (async message processing engine). It controls subscription behavior, outbox processing, and handler-specific settings.

[server]
# Default subscription settings
default_subscription_type = "stream"      # "stream" or "event_store"
default_subscription_profile = "production"  # Profile for defaults
messages_per_tick = 100                   # Messages per processing cycle

# StreamSubscription defaults
[server.stream_subscription]
blocking_timeout_ms = 5000    # Blocking read timeout
max_retries = 3               # Retries before DLQ
retry_delay_seconds = 1       # Delay between retries
enable_dlq = true             # Enable dead letter queue

# EventStoreSubscription defaults
[server.event_store_subscription]
position_update_interval = 10  # Messages between position writes
max_retries = 3                # Retries before marking exhausted
retry_delay_seconds = 1        # Delay between recovery retries
enable_recovery = true         # Enable periodic recovery pass
recovery_interval_seconds = 30 # Interval between recovery sweeps
gap_timeout_seconds = 5        # $all subs: hold at a global_position gap this long before abandoning it

# BrokerSubscription defaults
[server.broker_subscription]
max_retries = 3               # Retries before DLQ
retry_delay_seconds = 1       # Delay between retries
enable_dlq = true             # Enable dead letter queue

# Version conflict auto-retry
# Retries ExpectedVersionError at the handler level before
# the error reaches the subscription retry/DLQ pipeline
[server.version_retry]
enabled = true               # Enable/disable auto-retry
max_retries = 3              # Fast retries before propagating
base_delay_seconds = 0.05    # 50ms initial backoff
max_delay_seconds = 1.0      # Cap backoff at 1 second

# Transient-failure auto-retry (distinct from version_retry)
# Retries handlers that fail with transient infrastructure errors
# before the error reaches the subscription retry/DLQ pipeline.
# Opt-in: disabled by default. Per-handler retries=/backoff=/
# retry_exceptions= options override these.
[server.transient_retry]
enabled = false              # Off by default; turn on to retry transient errors
max_retries = 3              # Retries before propagating
backoff = "exponential"      # exponential | linear | fixed
base_delay_seconds = 0.1     # Initial backoff delay
max_delay_seconds = 5.0      # Cap backoff at 5 seconds
# Only genuinely transient exceptions belong here. Dotted paths or
# bare builtin names.
exceptions = [
    "builtins.ConnectionError",
    "builtins.TimeoutError",
    "protean.exceptions.SendError",
]

# DLQ maintenance (retention + alerting)
# Off by default; opt in by setting enabled = true.
[server.dlq]
enabled = false              # Master switch for the maintenance task
retention_hours = 168        # Trim DLQ entries older than this (default: 7 days)
alert_threshold = 100        # Warn when a DLQ stream reaches this depth
check_interval_seconds = 60  # How often the maintenance cycle runs
alert_callback = "myapp.alerts.notify_oncall"  # Optional dotted path, called on breach

# Kubernetes-compatible health HTTP server
# Enabled by default on port 8080; disable for tests or embedded use.
[server.health]
enabled = true               # Start the built-in health server
host = "127.0.0.1"           # Bind address (loopback; use "0.0.0.0" to expose)
port = 8080                  # Listen port
port_auto_increment = false  # Try 8081, 8082, ... if the port is taken

# Two-lane event/command routing (backfill vs. primary stream)
# Off by default; opt in by setting enabled = true.
[server.priority_lanes]
enabled = false               # Off by default; enable two-lane routing
threshold = 0                 # Priority below this routes to the backfill lane
backfill_suffix = "backfill"  # Suffix for the backfill stream/DLQ

# Handler-specific overrides
[server.subscriptions.OrderEventHandler]
profile = "fast"
messages_per_tick = 50
# Per-subscription DLQ overrides (inherit from [server.dlq] when unset)
dlq_retention_hours = 72
dlq_alert_threshold = 25

[server.subscriptions.InventoryProjector]
subscription_type = "event_store"
profile = "projection"

Subscription Profiles

Pre-configured settings for common scenarios:

Profile Type Use Case
production stream High throughput with reliability
fast stream Low-latency processing
batch stream High-volume batch processing
debug stream Development and debugging
projection event_store Building read models

Read more in Server → Configuration section.

DLQ Maintenance

The [server.dlq] section configures a periodic maintenance task that trims old DLQ entries and alerts when DLQ streams grow deep. It is disabled by default, set enabled = true to activate it. Requires a broker that implements the DLQ contract (currently Redis Streams).

Key Type Default Description
enabled bool false Master switch. When true, the Engine starts the maintenance task.
retention_hours int 168 Trim DLQ entries older than this window (default 7 days).
alert_threshold int 100 Emit a WARNING log and invoke alert_callback when a DLQ stream reaches this depth.
check_interval_seconds int 60 How often the maintenance cycle runs.
alert_callback str | None None Dotted import path to a callable invoked on threshold breach. Signature: (dlq_stream: str, depth: int, threshold: int) -> None. Exceptions raised by the callback are caught and logged.

Per-subscription overrides are available on each handler's [server.subscriptions.<HandlerName>] block:

Key Type Default Description
dlq_retention_hours int | None inherit Override retention_hours for this subscription's DLQ stream only.
dlq_alert_threshold int | None inherit Override alert_threshold for this subscription's DLQ stream only.

For the operational workflow (discover, inspect, replay, purge) see Dead Letter Queues.

Health Checks

The [server.health] section configures the built-in HTTP server used for Kubernetes liveness and readiness probes. The server is enabled by default on port 8080.

Key Type Default Description
enabled bool true Start the health HTTP server. Set to false for tests or embedded deployments.
host str "127.0.0.1" Bind address. Defaults to loopback so probes are not exposed off-host; set "0.0.0.0" for out-of-pod probes.
port int 8080 Listen port.
port_auto_increment bool false When true, if port is already bound the server walks up (8081, 8082, ...) until it finds a free one. Lets several engines share a host without colliding.

The server exposes GET /healthz, GET /livez, and GET /readyz. For the probe response bodies, readiness semantics, and FastAPI router factory, see Server Hardening reference.

Priority Lanes

The [server.priority_lanes] section configures two-lane event/command routing: production messages flow through the primary stream while bulk/migration messages are routed to a separate backfill stream. It is disabled by default: set enabled = true to activate it.

Key Type Default Description
enabled bool false Whether to activate priority lanes. When false, all messages use a single stream regardless of priority.
threshold int 0 Priority values strictly below this threshold are routed to the backfill lane.
backfill_suffix str "backfill" Suffix appended to the stream category to form the backfill stream name.

Read more in Priority Lanes and Using Priority Lanes.

outbox

This section configures the transactional outbox pattern for reliable message delivery. The outbox is automatically enabled when server.default_subscription_type is set to "stream". The legacy enable_outbox = true flag still works for backward compatibility.

[server]
default_subscription_type = "stream"  # Enables outbox automatically

[outbox]
broker = "default"         # Target broker for publishing
messages_per_tick = 10     # Messages per processing cycle
tick_interval = 1          # Seconds between cycles
max_tick_interval = 30     # Adaptive backoff cap when idle (omit to disable backoff)

# Retry configuration
[outbox.retry]
max_attempts = 3           # Maximum retry attempts
base_delay_seconds = 60    # Initial retry delay
max_backoff_seconds = 3600 # Maximum retry delay
backoff_multiplier = 2     # Exponential backoff multiplier
jitter = true              # Add randomization to delays

# Cleanup configuration
[outbox.cleanup]
published_retention_hours = 168   # Keep published for 7 days
abandoned_retention_hours = 720   # Keep abandoned for 30 days
batch_size = 5000                 # Rows deleted per bounded cleanup batch

Cleanup runs as bounded batch_size deletes rather than one unbounded DELETE, so large backlogs are cleared without long lock holds; each batch commits before the next when cleanup runs standalone.

Read more in Server → Outbox Pattern section.

telemetry

This section configures OpenTelemetry distributed tracing and metrics.

[telemetry]
enabled = true
service_name = "my-service"
exporter = "otlp"
endpoint = "http://localhost:4317"

[telemetry.resource_attributes]
"deployment.environment" = "production"
Key Type Default Description
enabled bool false Master switch for all OTel instrumentation
service_name string domain name Populates the OTel service.name resource attribute
exporter string "otlp" Span and metric exporter: "otlp" or "console"
endpoint string SDK default OTLP collector endpoint (gRPC)
resource_attributes table {} Additional OTel resource attributes

When disabled (the default), all tracing and metrics are no-ops with zero overhead. Requires pip install "protean[telemetry]".

Read more in OpenTelemetry Integration.

idempotency

This section configures command idempotency deduplication. When configured with a Redis instance, domain.process() can detect and deduplicate repeated command submissions using caller-provided idempotency keys.

[idempotency]
redis_url = "redis://localhost:6379/5"  # Redis connection URL
ttl = 86400                              # Success entry TTL in seconds (default: 24 hours)
error_ttl = 60                           # Error entry TTL in seconds (default: 60s)
Key Description Default
redis_url Redis connection URL for the idempotency cache. When None, deduplication is disabled. None
ttl Time-to-live for successful idempotency entries (seconds). After expiry, the same key can be reused. 86400 (24 hours)
error_ttl Time-to-live for error entries (seconds). Short TTL allows retries after transient failures. 60

Without redis_url configured, idempotency deduplication is disabled and domain.process() works normally with no errors.

Read more in Command Idempotency.

consume_idempotency

This section configures cleanup of the consume-side idempotency markers used by projectors to detect and skip already-processed messages (the ProcessedMessage marker; distinct from the command idempotency store above). See ADR-0017.

[consume_idempotency.cleanup]
retention_hours = 168  # Prune markers older than this (default: 7 days)
batch_size = 5000      # Rows deleted per bounded cleanup batch
Key Description Default
cleanup.retention_hours Prune ProcessedMessage markers older than this window (hours). 168 (7 days)
cleanup.batch_size Rows deleted per bounded cleanup batch. 5000

Read more in Projectors and the protean data idempotency CLI command.

logging

This section configures Protean's structured logging: level, output format, file rotation, slow-handler/slow-query thresholds, and redaction of sensitive fields.

[logging]
level = "INFO"
format = "auto"

Full key reference, redaction rules, and tail sampling: Logging.

lint

This section configures the diagnostics reported by protean check. All keys are optional.

[lint]
level = "warn"                        # exit-code severity floor
aggregate_size_limit = 5              # entities before AGGREGATE_TOO_LARGE
handler_breadth_limit = 5             # message types before HANDLER_TOO_BROAD
check_infra_imports = false           # flag domain modules importing protean.adapters
check_adapter_calls = false           # flag domain elements calling adapters directly
rules = ["my_app.lint.check_names"]   # dotted paths to custom rule callables

[lint.suppressions]
UNHANDLED_EVENT = 3                    # grandfather the first 3 findings
Key Type Default Description
level str "warn" Exit-code severity floor for protean check. "error" fails only on errors; "warn" fails on errors and warnings; "info" fails on any error, warning, or info finding. Errors always exit 1; a gating non-error finding exits 2.
aggregate_size_limit int 5 Entity count above which an aggregate emits AGGREGATE_TOO_LARGE.
handler_breadth_limit int 5 Message-type count above which a handler emits HANDLER_TOO_BROAD.
check_infra_imports bool false When true, protean check AST-parses each resolvable domain element's source module (skipping elements with no file-backed module and imports guarded by TYPE_CHECKING, try/except, or a function body) and emits INFRA_IMPORT_IN_DOMAIN for any top-level import from protean.adapters. Off by default because it reads source files.
check_adapter_calls bool false When true, protean check emits ADAPTER_CALL_IN_DOMAIN for a domain element calling into an adapter directly rather than through a port. Off by default because it reads source files.
rules list[str] [] Dotted import paths to custom lint callables with the signature (ir: dict) -> list[dict].
suppressions table {} A {CODE: N} map that grandfathers the first N findings of each code, in a deterministic (code, element, field, message) order. Findings beyond N are still reported.

level sets the CI failure floor without hiding findings; the --level CLI flag only filters what is displayed and never changes the exit code.

Individual elements can silence specific codes for themselves with the suppress_checks decorator option (for example @domain.aggregate(suppress_checks=["AGGREGATE_TOO_LARGE"])), which takes precedence over the [lint.suppressions] allow-list.

[server.partitioning]

Tunables for sequential_by partition ownership. Defaults suit most deployments; raise the lease TTL only if your handlers routinely pause longer than it.

[server.partitioning]
lease_ttl_ms = 15000              # how long an ownership lease survives without renewal
heartbeat_interval_seconds = 3.0  # how often the owner renews it
poll_interval_seconds = 0.25      # how often a worker looks for unowned partitions
reap_idle_ms = 3600000            # idle time before a cold partition is pruned
Key Type Default Purpose
lease_ttl_ms int 15000 How long a partition's ownership lease survives without renewal. A worker that dies frees its partitions after this.
heartbeat_interval_seconds float 3.0 How often the owner renews the lease. Keep it well under lease_ttl_ms.
poll_interval_seconds float 0.25 How often a worker looks for partitions it could take.
reap_idle_ms int 3600000 How long a partition must be empty and idle before it is pruned from the index.

See Sequential processing for the model these configure, and ADR-0028 for why ownership is leased and fenced.

Custom Attributes

Custom attributes can be defined in toml under the [custom] section (or [tool.protean.custom] if you are using the pyproject.toml file).

Custom attributes are also made available as domain attributes.

debug = true
testing = true

[custom]
FOO = "bar"
In [1]: domain = Domain()

In [2]: domain.config["custom"]["FOO"]
Out[2]: 'bar'

In [3]: domain.FOO
Out[3]: 'bar'

Multiple Environments

Most applications need more than one configuration. At minimum, there should be separate configurations for production and for local development. The toml configuration file can hold configurations for different environments.

The current environment is gathered from an environment variable named PROTEAN_ENV.

The string specified in PROTEAN_ENV is used as a qualifier in the configuration.

[databases.default]
provider = "memory"

[staging.databases.default]
provider = "sqlite"
database_url = "sqlite:///test.db"

[prod.databases.default]
provider = "postgresql"
database_url = "postgresql://postgres:postgres@localhost:5432/postgres"

Protean has a default configuration with memory stubs that is overridden by configurations in the toml file, which can further be over-ridden by an environment-specific configuration, as seen above. There are two environment specific settings above for databases - an sqlite db configuration for staging and a postgresql db configuration for prod.

Testing Overlays

A common use of environment overlays is dual-mode testing, running the same test suite against in-memory adapters for speed and real infrastructure for confidence. Define a [test] overlay (the default for pytest) and a [memory] overlay, then switch from the command line:

# Base configuration (development)
[databases.default]
provider = "postgresql"
database_uri = "postgresql://localhost/myapp_local"

[brokers.default]
provider = "redis"
URI = "redis://localhost:6379/0"

# Test overlay: separate database, sync processing
[test]
testing = true
event_processing = "sync"

[test.databases.default]
database_uri = "postgresql://localhost/myapp_test"

# Memory overlay: all in-memory adapters, no Docker needed
[memory]
testing = true
event_processing = "sync"

[memory.databases.default]
provider = "memory"

[memory.event_store]
provider = "memory"

[memory.brokers.default]
provider = "inline"
pytest --protean-env memory   # Fast — in-memory, no infrastructure
pytest                        # Thorough — real adapters (PROTEAN_ENV=test)

Protean's pytest plugin sets PROTEAN_ENV before test collection via the --protean-env option (default: test). See the Dual-Mode Testing pattern for the full approach.