Python DDD Framework
Build modular Python applications with explicit domain boundaries, transactional application services, and a consistent path from local development to deployment.
Python DDD Framework combines a module system with FastAPI, Dishka, Pydantic, SQLAlchemy, PostgreSQL, and Redis. It includes the pddd CLI and project templates, so you can create an application, add business modules, manage their database migrations, and run the same application through HTTP, background jobs, or direct service calls.
Python 3.12–3.14 · Windows and Linux · Async application services · Typed public APIs
Features · Quick start · CLI reference · Application development · Deployment · Upgrading · Documentation
Features
| Capability | What you can build with it |
|---|---|
| Modular applications | Declare module dependencies, compose a Host, and run ordered initialization and shutdown with application-local state. |
| Domain-driven design | Define aggregates, value objects, repository contracts, domain events, and optimistic concurrency rules. Keep domain models separate from DTOs and ORM models. |
| Application services | Use typed service contracts and a shared invocation pipeline for validation, authorization, interceptors, auditing, and units of work. |
| Dependency injection | Register services and native Dishka providers, discover implementations within module-owned packages, and manage application, request, and action scopes. |
| Configuration | Bind YAML and environment inputs to typed Options, validate them at composition time, and inspect configuration sources with sensitive values redacted. |
| HTTP APIs | Expose selected application services through FastAPI, or write explicit routers. Use OpenAPI, filters, file uploads, downloads, and streaming responses. |
| Persistence and events | Use async SQLAlchemy repositories, explicit transaction boundaries, module-owned Alembic migrations, seed contributors, and local events before or after commit. |
| Identity and permissions | Add JWT authentication, refresh sessions, users and roles, permission definitions, and authorization on service methods. |
| Cache, settings, and locks | Use typed Redis caching, runtime settings with conditional updates and reset, settings refresh notifications, and explicit Redis-backed business locks. |
| Background execution | Enqueue durable PostgreSQL jobs with typed payloads, schedule recurring work, run periodic workers, and select in-process or managed subprocess execution. |
| Real-time communication | Add authenticated WebSocket connections, typed messages, and targeted notifications to connected clients. |
| Hosting and observability | Manage long-lived integrations with HostedService, coordinate shutdown, and use structured logging, correlation, OpenTelemetry tracing, and lifecycle health endpoints. |
| Developer tooling | Generate projects and business modules, inspect composition, run local infrastructure, and reuse TestApplication for application tests. |
The Module owns a feature and its dependencies. The Host composes modules, selects providers, and supplies configuration. Your application owns the generated source code and database migrations.
Runtime integrations ship in the base package; the Host explicitly enables the modules it needs. Installing the package does not start database connections, workers, or trace export. Local events stay within an application process, and WebSocket notifications do not include a distributed backplane or offline replay.
Quick start
Prerequisites
- A supported Python interpreter; the examples use Python 3.12.
- uv for tool installation, dependency management, and project commands.
- Docker with Compose, running locally, for the generated application's PostgreSQL and Redis services.
The supported versions and dependencies are defined in pyproject.toml. The framework, CLI, and bundled templates share one package version.
Version 0.6: This version separates Identity credential and management contracts, enforces stable aggregate event values, corrects joined Unit of Work diagnostics, and adds recoverable CLI module publication. Apply the migration instructions below; previous Identity imports have no compatibility aliases. Check release status for publication and validation evidence. Use the local-wheel route below when evaluating a checkout that has not been published.
1. Install the CLI and create a project
For a published release:
uv tool install --python 3.12 "python-ddd-framework[developer-kit]"
pddd new my-app --python 3.12
cd my-app
To select a specific release, append ==<version> to the quoted package requirement. pddd new creates the Host, configuration, tests, deployment files, and four application documents: README.md, AGENTS.md, docs/architecture.md, and docs/development.md. Python metadata, configuration, environment, source, tests, and deployment files live in backend/; root scripts/ belongs to the consumer. It then runs uv sync inside backend/ and pins both the runtime dependency and developer tools to the CLI's exact framework version. The destination directory must not already exist. --python accepts a supported portable major.minor[.patch] version. --no-sync writes files only and works without that interpreter installed; later run uv sync --project backend to create the target environment. Native uv/test/build and Docker commands run from backend/.
Using an unreleased local wheel
Obtain a wheel built from the checkout you want to evaluate. Replace <absolute-wheel-path> with its full .whl path, and use the same wheel for the CLI and the generated application:
uv tool install --python 3.12 "<absolute-wheel-path>[developer-kit]"
pddd new my-app --framework-wheel "<absolute-wheel-path>"
cd my-app
The generator checks that the wheel contains python-ddd-framework at the CLI's version, copies it into the application's backend/vendor/ directory, and records the local dependency source. Installing a CLI from source alone does not change where generated applications obtain their framework dependency.
2. Prepare infrastructure and add a business module
Run installed pddd project commands from the application root, backend/, or any descendant directory. They locate Host metadata and use the backend's environment and framework version; unrelated nested Python projects do not take ownership:
pddd dev-init
pddd add module orders --template ddd
pddd add module conversions --template basic
dev-init starts or reuses local PostgreSQL and Redis using backend/app.development.yaml. It preserves existing data; database migration and seeding are separate steps.
add module orders generates a DDD skeleton named orders and registers its Application, SQLAlchemy, and HttpApi Modules with the Host. It retains all responsibility directories, package markers, lifecycle hooks, ORM Base, model discovery, migration registration, and a module README. It creates no business entities, services, tables, endpoints, tasks, or test cases. Add them from confirmed requirements. The former order implementation is available as a separate order-management example, outside the runtime wheel.
ddd is the default six-layer template. basic generates only a Module, a synchronous Protocol service, its transient implementation, and an English README. It has no database/Redis requirement or implicit HTTP/interception. Both templates support --dry-run and refuse existing targets.
3. Initialize the database
Apply the provider migrations selected by the generated Host:
pddd db upgrade --module identity
pddd db upgrade --module settings
pddd db upgrade --module auditing
pddd db upgrade --module background_jobs
pddd db seed --module identity
An empty DDD skeleton retains model and migration registration but has no business tables or revisions. Add actual models before generating a business revision; migration commands still use the configured database. For a module named orders:
pddd db revision --module orders
Review the generated revision in backend/src/modules/orders/sqlalchemy/migrations/ before applying it. Run seed only after declaring the intended contributors:
pddd db upgrade --module orders
pddd db status --module orders
pddd db seed --module orders
Migration commands operate on the selected module. When a migration requires another module's revision, upgrade that prerequisite explicitly. Starting the Host does not migrate or seed the database.
4. Run the application and explore the API
pddd dev
Open http://127.0.0.1:8000/docs. Use the generated identity.seed_admin_username and identity.seed_admin_password in backend/app.development.yaml to call /api/auth/login, then enter the returned access token in Swagger's Authorize dialog.
The empty orders skeleton contributes no business routes. The independent order-management example supplies /api/orders, approval and queued approval, file transfer, and an authenticated WebSocket at /ws/orders; use its initializer when you want to explore those capabilities.
The default business API prefix is /api. Liveness and readiness are exposed at /health/live and /health/ready; readiness describes application lifecycle state, not continuous database or Redis health.
CLI reference
Use pddd to create a project. Inside an existing project, use pddd so commands run with that project's framework version.
| Command | Purpose |
|---|---|
pddd new my-app |
Generate a new Host project and install its dependencies. |
pddd add module orders |
Generate a business module and register its packages and Host dependencies. |
pddd dev-init |
Prepare the local PostgreSQL and Redis services from development configuration. |
pddd dev |
Start the development web server with reload at 127.0.0.1:8000. |
pddd db revision --module orders |
Generate an Alembic revision for the module's models. Review it before applying. |
pddd db upgrade --module orders |
Apply the selected module's migrations. |
pddd db status --module orders |
Check that the database is at the selected module's migration heads; fail if it is not. |
pddd db seed --module orders |
Run the selected module's seed contributors after checking its migrations. |
pddd inspect |
Print the composed module graph, service registrations, and redacted configuration inputs as JSON. |
Common variations:
pddd new my-app --dry-run
pddd add module billing --dry-run
pddd dev --host 127.0.0.1 --port 8080
pddd inspect --environment development
pddd db status --module orders --environment production
Generation supports --dry-run without writing files. Database commands require --module; aliases come from installed python_ddd_framework.modules entry points and must belong to the current application's module graph.
Recovering a failed module addition
add module prints a recovery directory under the identified backend's
.python-ddd-framework/generation/<operation-id>/. It retains original bytes, proposed
startup/metadata files, a module file manifest, step records and uv-sync.log. The rendered
template and any unfinished sibling copies are retained too. Open that operation's
RECOVERY.md for its paths and a copyable, read-only inspection procedure.
Stop other generators and editors before recovery. Compare the actual files with the
recorded digests: continue only from the recorded original content, or restore an original
file only while the target still matches this operation's proposed content. Review any
other content manually. Keep generated directories and markers until you explicitly decide
what to do with them; rerunning add module does not repair a partial publication.
When all project files match the proposed contents but installation failed, use the
independent sync script in that operation's RECOVERY.md. Run it with python -I as
instructed. It clears the CLI's recorded Python/uv environment overrides before running
uv sync in the identified backend, including overrides set after generation. A bare
uv sync --directory still inherits environment overrides and can select another
virtual environment. The retry preserves your shell environment and the original records.
File publication and installation have separate results. An interrupted or missing step record requires checking the files; it does not prove that the write failed. The CLI does not automatically restore, move or delete the failure site, roll back the environment or lockfile, or guarantee a transaction across files or power-loss recovery.
Project command environment
db and inspect use --environment, then PYTHON_DDD_FRAMEWORK_ENVIRONMENT, then development. dev always selects development, and dev-init is restricted to development infrastructure.
inspect builds and closes the application without starting services or running migrations. Its output describes a newly composed Host, not a running process. Configuration output contains input values and their sources; it does not include Options defaults or later module contributions.
For the complete options supported by your installed version:
pddd
pddd new --help
pddd add module --help
pddd db --help
pddd inspect --help
Application development
Find the right place for a change
The generated project starts with backend/src/host/ for composition and startup. Modules live under backend/src/modules/. A basic module contains module.py, contracts/, services/, and its README. The default ddd module has these responsibilities:
| Location within a module | Responsibility |
|---|---|
domain_shared/ |
Shared values, errors, and permission names. |
domain/ |
Aggregates, business rules, events, repository contracts, settings, and seed contributors. |
application_contracts/ |
DTOs and public application-service contracts. |
application/ |
Use-case orchestration, service implementations, event handlers, jobs, and workers. |
sqlalchemy/ |
ORM models, repository implementations, and migrations. |
http_api/ |
Service exposure, explicit routes, file transfer, and WebSocket endpoints. |
tests/ |
Tests owned by the business module; excluded from the production wheel. |
For a new use case, define its input and output in application_contracts/, put business rules in domain/, and coordinate the work in application/. Persist through a repository contract. The HTTP module selects which application services to expose; the Host selects infrastructure providers.
For a model change, edit sqlalchemy/models/, generate a revision, review it, then run db upgrade for that module. For another business module, repeat the add module, db revision, db upgrade, and db seed steps. Add application-specific dependencies with uv add <package>.
See the development guide for service registration, typed invocation, HTTP declarations, transactions, background handlers, and distributed locks.
Configure the application
Edit backend/app.development.yaml for local connections, identity settings, logging, and module Options. An optional backend/app.yaml can hold shared configuration. Environment variables override file inputs; generated Hosts use the PDDD_ prefix and __ between nested fields. For example, PDDD_HTTP__API_PREFIX=/v1 changes the business API prefix.
PYTHON_DDD_FRAMEWORK_ENVIRONMENT selects the Host environment; it is separate from business configuration. Typed Options validate configuration during application composition. Use pddd inspect to investigate input values and where they came from.
Generated credentials are for local development. Production configuration is supplied separately in app.production.yaml. See configuration and background and logging configuration.
Run application tests
After completing local database setup, run from backend/:
uv run pytest
The generated application includes module tests and Host tests. Keep Docker available for tests that use real infrastructure. For custom application tests, the framework's TestApplication reuses the normal application builder and typed service invocation. Framework contributor checks are documented separately in framework validation.
Deployment
Generated applications include backend/Dockerfile and backend/compose.production.yaml. Run build and deployment commands from backend/:
uv build --no-sources
For container deployment:
- Supply
app.production.yamlwith production connections and credentials; keep it out of source control and the image. The generated Compose file mounts it read-only for both migration and web services. - Build and validate the application image, then set
APP_IMAGEto that image's immutable digest. - Run database upgrades for each required module in dependency order. Run initial seed operations explicitly with
--environment production. - Start the web service from the same image and check its health endpoints.
The image uses python -m host.main without development reload. Runtime installations use the base framework package; developer-kit is only needed for generation. Follow the Docker deployment guide for commands and operational boundaries.
DDD skeleton generation (0.7)
New ddd modules contain the full responsibility directories and necessary composition, with no preset business code or tests. basic is unchanged. This affects subsequent generation only: upgrades do not delete or rewrite existing consumers' order code. Keep existing business contracts and data under their application owners. The former order implementation and its existing domain test now live in the independent example; its initializer uses the official CLI, requires a new destination, and is not bundled in the runtime wheel. No CLI parameter or dependency has been added.
Streaming and access migration (0.7)
- Move
fastapi_realtime.authentication_refresh_secondstofastapi_access.authentication_refresh_seconds. The sharedFastApiAccessOptionsapplies to managed HTTP streams and WebSocket connections; its default is 60 seconds and minimum is 1. The old realtime field is rejected. - HTTP endpoints on
HttpRoutercan directly yield synchronous or asynchronous values using native FastAPI SSE, JSONL, orStreamingResponse. Keep checks required before the response starts in preparation dependencies or Filters. Authorization precedes binding; function dependencies, commit, and business resource cleanup finish before sending. The framework does not read the first item during preparation. Application services keep their existing return contract and may return a native streaming Response. - Register subscriptions acquired during preparation with a native request-scoped yield dependency. An unstarted generator's
finallycannot release those subscriptions. Database work in preparation or during iteration must use an independent short service call or UoW and finish before yielding data. Generator cleanup that awaits must shield its ownfinally, without holding a cancellation scope acrossyield. - Streams with authorization declarations automatically recheck identity and endpoint permissions, including while idle or blocked on a slow client. Anonymous declarations retain their meaning. WebSocket
invokecontinues to authorize each service call separately. Failed checks terminate the transport; afterhttp.response.start, no replacement JSON response is sent. run_host(..., timeout_graceful_shutdown=10)passes a non-negative integer to Uvicorn. When that period expires, cancellation is requested and framework drain still waits for actual resource release. Direct Uvicorn consumers must configure its native timeout themselves. A synchronous generator's runningnext()must eventually return; a timeout does not abandon its thread.
Runnable examples: HTTP generators. Verification and limitations: 0.7.0 status.
Capability module migration (0.4)
Capability composition now uses the same AppModule model as customer modules. Existing applications must migrate their declarations together; there is no compatibility path.
- Declare capability dependencies on the Module that uses them. Selecting a sibling provider in Host does not supply a missing business-module dependency.
ApplicationServicesModulesupplies the usual application-service capabilities; additional capabilities are explicit dependencies. - Wrap capability declarations with
NotificationDefinitions,HostedServices,BackgroundWorkers,BackgroundSchedules, orSettingHandlersfrom their owning packages. Use native service registration for providers andServiceCompositionfor capability discovery and final binding. Keep provider selection in Host. - Replace Application invocation methods with
invoke(application, Service.method, ...),invoke_as(application, user, ...),call(application, function, ...), andcall_as(application, user, ...). Register interception withadd_interceptor(context, ...)and events withsubscribe_local_event(context, ...). Import these functions frompython_ddd_framework. - Read capability catalogs from their Module instance through
application.modules.instance(ModuleType)or the catalog's DI binding. Generic service diagnostics remain inapplication.service_catalog; readiness isapplication.is_ready. DataSeedContributor,DataSeeder, andDataSeedErrorbelong topython_ddd_framework.seedingand remain available from the root package. Domain Modules with contributors depend onDataSeedingModule; remove@application_servicefrom contributors. Seed is no longer an ApplicationService or part of its catalog. Explicit seed commands retain contributor ordering, per-item UoW, module dependency selection, failure reporting, and cleanup.- HTTP Modules depend on
FastApiModule(orFastApiRealtimeModulefor its additional capability). The module registers the native FastAPI integration; remove manualFastapiProvidercontributions from Host. - Ordinary DI services can opt into
@unit_of_work,@authorize, andValidationEnabled. Declare their capability dependencies on the service's owning Module and retain the existing native registration. Only marked async instance methods enter interception; class declarations covering synchronous/static/class/generator methods fail at build. Usecall/call_asfor non-DI callers. These services do not become ApplicationServices or HTTP endpoints. - Interceptor match functions receive
ServiceMethod, replacingApplicationServiceMethod. Shared method descriptions, validation policies, interception contracts and execution now belong topython_ddd_framework.invocation; import the public symbols from the root package. The old name and movedapplication_servicesimplementation modules have no forwarding aliases. ApplicationService retains its REQUEST-to-ACTION proxy.
The current contracts are in architecture, the decisions are ADR-015 and ADR-032, and validation limits are in status. Slice 3 data/event migration is documented below; later slice status is recorded on the status page.
Data and event migration (0.4)
- Replace
SqlAlchemyRepository.addwithinsert. Usefindfor optional results;getnow raisesResourceNotFoundErrorwhen absent. List/page return lists, insert/update return the entity, and delete/batch methods returnNone.auto_save=Trueflushes the owning UoW without committing an outer transaction. Explicit mapping repositories retain their own business methods; update their callers and use the new names where they expose the standard operations. - Manual
UnitOfWorkManager.begin()creates missing execution scopes and owns their exit. Callcomplete()explicitly. Existing scopes, task/connection limits, outer participation andrequires_newownership remain intact. - Replace synchronous
publish_domain/publish_after_commitwithawait events.publish(event, phase=LocalEventPhase.DOMAIN/AFTER_COMMIT), or omit phase to notify both. No UoW dispatches immediately; transactional UoW defers by default; nontransactional UoW rejects explicit and automatic events before repository writes.on_unit_of_work_complete=Falsenever advances AFTER_COMMIT ahead of a successful commit. - Replace
CacheDefinition/CacheDefinitions/CacheCatalogwith injectedDistributedCache[ValueType]; remove cache declarations from Modules. Use@cache_name("stable.name")on the value type if required. Calls take keys/values directly. Configure globalcaching.default_entry_optionsor pass a completeDistributedCacheEntryOptions; omitted options use the default 20-minute sliding expiration, while an empty options object is persistent. Development Hosts explicitly setcaching.hide_errors: false. - Redis cache keys and entry encoding changed; old disposable entries are not read or migrated. They expire under their old TTL.
consider_uow=Truestages writes/deletions per concrete UoW; completion failures retain the committed database fact and do not make the two resources atomic. Do not retry an entire committed business operation merely because an AFTER_COMMIT handler or cache write failed.
Aggregate event values (0.6)
AggregateRoot.raise_local_event now checks nested field values before enqueueing. Events must be frozen dataclass instances or frozen Pydantic models whose fields follow the aggregate event value contract.
- Replace mutable containers with tuples, frozensets, or explicit frozen value records. Decorating a mutable-container subclass with
frozen=Truedoes not make it an accepted event value. Map entity references to the IDs and values the event needs at the publishing point. AMappingProxyTypestill exposes changes to its backing dictionary and is rejected. - Remove private, cached, or undeclared instance state from event records. Pydantic models with
extra="allow"are rejected. Dataclass subclasses must themselves be decorated withfrozen=True. - Normalize database-driver scalar subclasses at the repository mapping boundary. For example, rebuild an asyncpg UUID as
UUID(bytes=row.id.bytes)before passing it to a domain aggregate; the event validator accepts nativeuuid.UUIDvalues. - Rejected events raise
TypeErrorbefore altering the aggregate queue. Accepted events retain their identity; the framework does not copy or convert them. DirectLocalEventBus.publishand repository entity-change notifications retain their existing behavior.
Redis batch calls keep serial I/O to preserve first-error, cancellation, and expiry side effects. Cache keys, entry encoding, and UoW pending-write behavior retain their existing contracts.
Message processing (0.4)
Opt in with MessagingModule, MessageChannelDefinition[T], MessageHandler[T] and the
APP-scoped MessageChannel[T]. Register a concrete Handler as a cached ACTION single binding.
send() returns an acceptance receipt; receipt.wait() reports processing and cleanup, not
commit or external acknowledgement. submit() is the bounded external-thread entry. Only
replaceable snapshots may declare snapshot_key. Existing events, Jobs, WebSockets and hosted
callbacks retain their semantics. See the usage guide and
reference module. Existing applications may explicitly
adopt the sample; upgrades do not rewrite their code or generated documentation.
User entry migration (0.4)
@exposes_application_services(ContractsModule)now contributes the formal Module dependency. Remove the repeated ContractsModule entry from the HTTP Module'sdependencies; keep required transport capabilities. Conventional methods remain automatic,remote_service(is_enabled=False)excludes them, and route overrides still handle deliberate URL/operation identity differences.- A plain Pydantic DTO on a generated GET endpoint binds to query parameters. Use explicit
Body,Query,HeaderorDependsto select another native binding. Applications that intentionally sent GET bodies must now declareBody; complex parameter shapes retain FastAPI's native limits. - Construct schedules as
BackgroundJobSchedule(name="orders.daily", cron="0 0 * * *", job=JobType, payload=Payload(...)). A registered function'sBackgroundJobDefinitionis also accepted. Remove caller-suppliedjob_name,job_versionand JSON bytes. Keep existing schedule names and Job definitions to retain durable identity; enqueue and PgQueuer storage are unchanged. - Pass a
RealtimeMessagesubclass instance tosend_to_user(user_id, message),send_to_connection(connection_id, message)orconnection.send(message). Put the existing versioned name in the class'smessage_typeand declare its payload fields with Pydantic. Preserve existing names/fields when migrating clients. The envelope and queue acceptance report retain their meaning. locks.acquire(key, wait_timeout=timedelta(...))overrides only that acquisition's waiting time;Noneuses configured waiting and zero tries without waiting. Lease/renewal settings remain global.SettingManager.updatenow pairsSettingDefinition[T]withvalue: T; validate dynamic input at its boundary. Version tokens, reset and refresh behavior are unchanged.BusinessError(..., data=PublicData(...))optionally publishes a Pydantic DTO undererror.data; absence omits the field. Include only intentionally public fields, never raw exceptions or SQL.
See the usage guide for typed examples. Slice 5 is implemented; current extension/Identity migrations are documented below; the new consumer layout remains pending.
CLI module layout migration (0.4)
New modules use the responsibility directory rules, delivered inside each generated project. Existing applications are not regenerated by pddd upgrade.
- Replace the former
<Module>ApplicationServicewith<Module>QueryApplicationService,<Module>ManagementApplicationService, and<Module>ApprovalApplicationService; the consumer item example has a separate item service. Inject the contract for the called use case. - Move contracts from
application_contracts/orders.pyintoservices,inputs, andviews; internal reporting goes inintegration_services. Move aggregate/value/event/repository declarations into their Domain responsibility directories. Import the precise owner, without legacy aliases. - Shared immutable values used by Domain and public DTOs belong in
domain_shared/value_objects; domain-only values remain indomain/value_objects. The Catalog consumer movesMoneyout ofdomain_shared/values.pywithout changing its validation or normalization. - Move
application/tasks.pydeclarations intobackground_jobs/order_approval,background_workers, and their capability files. Hosted integration belongs inhosted_services; ORM Base belongs insqlalchemy/models/base.py. Update tests and explicit registrations along with imports. - Preserve external HTTP identities through the existing public route overrides. Keep Worker admission and HostedService/schedule enablement explicit; do not change existing schema, transactions, event ordering, or resource cleanup as part of this source migration. Merge the generated AGENTS/development conventions into application-owned documentation deliberately.
Identity migration (0.4)
Slice 7 adds user/role management and an application-local authorization switch. Existing public service contracts change without compatibility aliases.
- Apply
pddd db upgrade --module identitybefore running the new code. The newidentity_0004revision addsconcurrency_versionto users and roles with a non-null initial value; published revisions and token formats remain unchanged. Consumer extension columns/indexes and revisions stay in the consumer project. get_users(query)andget_roles(query)acceptPagedResultRequestDtoand returnPagedResult(items,total_count). Update callers that expected a bare array, custom repository implementations, and subclasses of the public defaults. New management DTOs are exported frompython_ddd_framework.identity.- Read
concurrency_versionfrom the current user/role or relationship view and supply it for edits, activation, deletion, passwords, and whole-set relationship updates. Relationship writes return the resulting version. HTTP DELETE takes a JSON version command. Handle 409 by rereading and resolving the edit; do not replace the supplied version and retry blindly.permission_versionremains internal authorization cache state. - Current-user password changes use
ChangePasswordCommandwith the old password and the version returned byme(). Administrator resets useSetPasswordCommandand the existing user-management permission. Password changes, deactivation, and deletion invalidate existing sessions after commit. Permission changes are reconsidered on subsequent requests. - Access/refresh lifetimes and lockout controls now belong to
IdentityOptions; defaults are preserved. Replace imports of the removed module-level duration/attempt constants with the owning Options. Existing refresh sessions use the currently configured lifetimes. AuthorizationOptions.always_allowdefaults toFalse. Configureauthorization.always_allowin the Host when authorization bypass is intended. It covers shared service/HTTP authorization without synthesizing identity or accepting invalid tokens; business validation, transactions, password checks, and realtime identity requirements remain active. Keep permission declarations so disabling the setting restores normal authorization.
See the Identity guide and validation limits. Consumer delivery is described in the layout migration below; fresh acceptance and remaining release limits are recorded on the status page.
Identity contract refactor (0.6)
Version 0.6 replaces IdentityRepository with two independent contracts exported from python_ddd_framework.identity:
IdentityCredentialStore:get_userandset_password, including the existing concurrency version and expected password hash.IdentityManagementStore: user/role creation, queries, edits, deletion and relationship updates. Password writes belong to the credential contract.
Authentication now receives credentials; management receives store and credentials; administrator seeding receives store. Update custom providers and directly constructed or subclassed defaults to these contracts. A shared implementation can use Dishka's native registration:
from dishka import AnyOf, Provider, Scope
from python_ddd_framework.identity import IdentityCredentialStore, IdentityManagementStore
provider = Provider()
provider.provide(
MyIdentityStore,
provides=AnyOf[MyIdentityStore, IdentityCredentialStore, IdentityManagementStore],
scope=Scope.REQUEST,
)
MyIdentityStore is the consumer's implementation. The built-in SQLAlchemy provider registers its existing implementation this way. Providers may also implement and register the two contracts separately. Each implementation must keep the existing transaction and credential concurrency rules.
For direct submodule imports, use identity.options, identity.dtos, identity.models, identity.stores, and identity.extensions. identity.contracts owns application-service contracts and permission names. IdentityAdminSeedContributor is defined in identity.seeding, which IdentityModule scans. The former IdentityRepository export and old definition paths have no compatibility shims. This refactor changes no HTTP payload, token format, database schema or migration.
Extension migration (0.4)
- Identity exposes
DefaultAuthenticationApplicationServiceandDefaultIdentityManagementApplicationService. Specialize these public defaults and use the existing module override registration; keep policies on the final methods. Remove imports of the former private implementations. UserViewis now a PydanticExtensibleModel; construct it with named arguments.CreateUserCommand,UpdateUserCommand,UserView(including paged results), andIdentityUsershareIDENTITY_USER_EXTENSION. Declare typedExtensionPropertiesthroughModelExtensions; unknown fields are rejected. HTTP extension values remain insideextra_properties, including properties mapped to SQL columns.- Nested DTOs and native Pydantic generic containers use the same Application-specific schema. Property types must be closed, including type aliases; nested models and dataclasses must reject unknown fields. Keep explicit Query/Header/Body bindings on the implementation when using a separate service contract.
- Upgrade Identity with
pddd db upgrade --module identitybefore using the new JSON container (identity_0003). Never edit an applied base revision. A custom Identity store must preserveIdentityUser.extra_properties; SQL stores use the Application'sSqlAlchemyModelCatalog.extension_modelbinding for queries and reads/writes. - For independent columns, the consumer persistence Module contributes
SqlAlchemyModelExtensionandSqlAlchemyExtensionMigrations. Register that Module's CLI alias in project metadata, create its local migrations package, and declare the required base revision independs_on. Do not register the base table or Schema again. - After upgrading the base, run
pddd db revision --module employee, review the generated file in the consumer extension module, then runpddd db upgrade --module employeeandpddd db status --module employee. Replaceemployeewith your declared alias. Generation includes only that extension's declared columns/indexes; installed framework files remain read-only. A standalone extension upgrade rejects missing prerequisites; the programmatic global migrator follows Alembic dependencies. - When a later extension change requires a newer base revision, update the declaration's
depends_on, apply that base revision, then generate the next extension revision. Earlier extension files keep their original dependencies. Upgrade/status require each current extension head to satisfy the current declaration. - Plan explicit data migrations for backfills, required new properties on existing rows, or destructive changes. Host startup never changes tables. Declaration and storage examples are in the extension guide.
Upgrading
Framework upgrades update dependencies. They do not regenerate your business code, Host, configuration, templates, or migrations. Updating a globally installed CLI also leaves existing projects unchanged.
- Read the target release's migration notes. Pin both
python-ddd-frameworkin runtime dependencies andpython-ddd-framework[developer-kit]in the development group to the same target version. - When adopting the unified installation layout, use
python-ddd-framework==<version>for runtime dependencies and remove the old capability extras. Legacy extra names and[all]are not retained. Preserve the application's own dependencies and sources. - When moving a local-wheel or old Git installation to PyPI, remove only the framework's entry in
[tool.uv.sources]. Keep unrelated source overrides and vendor files. - Apply any required source and configuration changes, update the environment, and run your application's validation:
uv lock --upgrade-package python-ddd-framework
uv sync --locked
uv run pytest
uv build --no-sources
During 0.x, compatible fixes use a patch release; changes requiring consumer adaptation use a minor release with migration instructions. Publishing changed package contents requires a new version. Release evidence, including the limits of previously tested upgrade paths, is recorded in status.
Migration notes
These notes remain the authoritative consumer migration instructions. Historical versions below refer to the former package name.
Unit of Work participant diagnostics (0.6)
An inner begin() that joins an existing Unit of Work now reads all transaction and cleanup facts from the owner. Completing or leaving the inner scope does not imply that the owner has committed or released its resources. Retained participant references continue to report the owner's latest outcome, including unknown commit results, saved batches and cleanup failures, while rejecting further operations after local completion or exit.
Do not use a participant's resources_released to detect exit from the inner scope; use the context manager boundary. Public types, signatures and provider hooks retain their existing contracts. This correction is introduced in 0.6.0.
Lifecycle shutdown migration (0.5)
Version 0.5.1 changes shutdown after the published 0.4.0 baseline. Custom RuntimeParticipant implementations must use the new global barriers: quiesce → stopping(reason) → close_admission → drain → stop(reason) → Module/container cleanup. Move accepted-work draining out of resource stop; keep dependencies alive until all drains complete. prepare(bool) still selects the startup mode.
Move business calls needed before resource release into the optional HostedService.stopping(context, reason) hook. Use its separate context for SDK/thread callbacks; the startup context stays closed. Only successfully started services receive the hook. stop still cleans every touched service, including partial startup failures. Each hook uses the existing shutdown_timeout; cancellation waits for actual cleanup and is not a total shutdown deadline.
Channels that must accept these callbacks declare stopping_owners=(ServiceType,) and pass the current hook context with send(value, stopping=context) or submit(value, stopping=context). The channel Module must depend on the declared service owner. The default permits no shutdown input. Capacity, receipts and per-call scope/UoW behavior remain unchanged; HTTP/WebSocket retain native transport shutdown. See the lifecycle decision and usage guide.
Additional fixes in 0.4.0
BackgroundJobRetryPolicy.max_attempts now counts all executions, including the first. A value of 2 permits two executions; 1 disables failure retries. The 0.3.2 PgQueuer mapping allowed one extra execution. If your application intentionally relied on that behavior, increase its configured total by one. BackgroundJobContext.attempt remains zero-based. No queue schema migration is required.
The developer-kit dependency constrains binaryornot to 0.5.x, because 0.6.0 can classify bundled templates containing Chinese comments as binary and skip rendering them. Re-resolve the developer-kit environment when installing 0.4.0. Updating the dependency does not replace existing generated application code.
The new DomainService marker uses existing constructor injection and transient REQUEST lifetime. It does not create transactions or expose HTTP endpoints. The independent order example places approval policy in domain/services/order_approval_service.py; existing projects can adopt it explicitly after upgrading. The example application checks existence and the submitted version before the domain policy; a stale or missing order therefore takes precedence over a disabled approval setting when both inputs are invalid. Domain rules, schema, and installation files are not automatically rewritten.
0.3.x — framework name, unified installation, and application guidance
The distribution is python-ddd-framework, the import package is python_ddd_framework, and the CLI is pddd. Neither python-platform nor the unreleased intermediate name python-modular-framework retains a package, import, or command alias. The intermediate name belongs to a different PyPI project. Version 0.3.1 carries this breaking migration; 0.3.0 did not pass the release gate and was not published. Publication evidence is tracked in release status.
- Replace dependency names, Python and dynamic imports, and entry point groups. Use
python_ddd_framework.hostsandpython_ddd_framework.modules. Replacepp/pmfcommands, framework wheel source keys, and deployment commands, then relock and rebuild without regenerating business code. - Use
python-ddd-framework==0.3.2for runtime dependencies andpython-ddd-framework[developer-kit]==0.3.2for development. Remove former capability extras; the base package includes runtime integrations while the Host still selects and enables providers. - New projects receive English setup, agent guidance, architecture, and development documents; newly generated modules receive a README. Existing applications are not regenerated or overwritten. Adopt the relevant guidance manually and preserve project-specific business rules and instructions.
- Replace
PYTHON_PLATFORM_ENVIRONMENTorPYTHON_MODULAR_FRAMEWORK_ENVIRONMENTwithPYTHON_DDD_FRAMEWORK_ENVIRONMENT. New templates usePDDD_for business configuration; existing applications may retain their own business prefixes. - Stop old Hosts and workers before switching. The local lock directory is now
.python-ddd-framework; old processes may still hold locks under the former directory. - Storage and protocol identifiers now use capability names. The default queue schema is
background_jobs, the default Redis prefix isapp, and generated applications use their project name as the Redis prefix. Table names, revisions, and branch labels follow their owning capability. WebSocket framework errors useframework.problem.v1. - There is no compatibility path for the old database, cache keys, or protocol identifiers. Recreate disposable development databases; explicitly migrate any data you need to retain. Installation identity remains configurable through
redis.key_prefix, PgQueuer Options, and SQLAlchemy migration Options. - Give installations sharing a database separate Alembic version table names or schemas. A version table cannot reside in the queue's exclusive schema. PgQueuer notification channels derive from the schema and object prefix and do not reuse the old channel. Changing these identities for an existing installation requires explicit migration of data and migration records. See configuration examples.
0.2.1 — application, HTTP, background, and persistence contracts
The 0.2 line introduced changes requiring application updates. Version 0.2.0 did not pass the full CI gate and was not released; the corrected release used the former python-platform name at 0.2.1. See the historical release record. For that release, both runtime and developer-kit dependencies were pinned to ==0.2.1. Consumption under the new name additionally requires the rename steps above.
- Repositories and service scopes: Repository interfaces inherit
RepositoryContract, Protocol; implementations belong in the persistence layer'srepositories/package and are scanned by their module. Remove manual scope and interface mappings. Repositories use cached REQUEST scope, application-service implementations use ACTION, andHostedServiceuses cached APP scope. Conflicting lifecycle declarations and non-cached providers are rejected. - Jobs and workers: Move job declarations to class attributes and discover them through
Module.scan_packages; enqueue withenqueue(JobType, payload). Register worker classes inModule.background_workers, with interval and enabled state on the class. Remove explicit class Definition registration andjobs.enabledconfiguration. Function registrations and durable name/version/payload identities remain unchanged. Approval payloads requireorder_id; drain or explicitly migrate old statistics jobs instead of passing them to the approval handler. - Hosted services and shutdown:
HostedServiceis opt-in. Hosted services start after module initialization and before background execution; the application reaches RUNNING only after all starts succeed. Shutdown waits for background execution first. Failed-start rollback waits for background, hosted-service, module, and container cleanup; cancellation propagates afterward with the original failure retained. - HTTP routes: Set the global prefix with
FastApiAdapter(api_prefix=...); generated Hosts readhttp.api_prefix. The defaultroot_pathno longer containsapp. Remove/apifrom module override paths and make method paths relative to the service path; a status-only HTTP declaration may omit its path. The default orders URLs are unchanged, while custom routes must be checked against OpenAPI. Explicit businessHttpRouterinstances useuse_api_prefix=True; independent routes remain separately declared. Managed routers preserve Host operation-ID generation and exclude the deployment prefix from generated IDs. - ORM models: Move models into
sqlalchemy/models/, withBaseowned by the package entry point and business models in separate files. UseSqlAlchemyModelRegistration.from_package, and update imports such asOrderRow. Keep migrations independently managed and execute upgrades explicitly. Function providers for closed generic repositories also undergo the fixed-scope checks. - Host configuration: Generated applications provide only
app.development.yaml. Deployment suppliesapp.production.yamlas a read-only mount outside source control and the image. Host and tests share the configuration entry point;dev-initderives Compose values from the composed snapshot. Replace private server entry points withpython_ddd_framework.fastapi.server.run_host. Module generation preserves dependency-tuple comments and formatting, and template imports are ordered for the consumer package.
0.1.1 — public package consumption
0.1.1was the first public-installation target under the former name.0.1.0did not complete public release; the patch retriggered publishing without changing API, dependency, or database contracts. See release evidence.- Project generation switched to exact PyPI dependencies while retaining same-version local-wheel validation.
--framework-sourceand implicit Git-source inference were removed. Applications moving from private prereleases pinned runtime and developer-kit dependencies to==0.1.1, removed only the framework source override, then relocked and validated. - Dockerfiles may remove Git/SSH tooling and credential mounts used solely to fetch the framework; assess other Git dependencies separately. This consumption change introduced no framework schema migration.
- New projects start with a Host skeleton and add business code through
add module. Existing applications keep their source layout. Check their Host/module entry points and database ownership against the development guide.
Earlier private versions
Apply only the migrations relevant to your application. Completed migrations do not need to be repeated.
| Former usage | Migration |
|---|---|
Application TOML, OptionDefinition/class metadata, Editor.patch, direct injection of concrete Options types |
Move to YAML configuration and pre_configure/configure with values; inject Options[T]. Preserve parent configuration hooks through super or a dependency on their owning module. No legacy loader or alias remains. |
Public ApplicationServiceInvoker or invoke(ServiceType, method, ...) |
Inject the public service proxy or use invoke(Service.method, ...). Put policies on the final implementation and signatures on the contract. Keep a separate contract for IntegrationService. |
Module Settings/permission definition lists or GlobalSettings |
Define synchronous SettingDefinitionProvider/PermissionDefinitionProvider classes inside scanned packages. Read through SettingProvider, write through SettingManager, and avoid unbound convenience properties during construction. |
HostedRuntime/Definition or ready/not-ready/failure callbacks |
Register HostedService types and implement async start/stop. Components own runtime failure and recovery; opt into background-process instances explicitly when required. |
SoftDeleteMixin, include_soft_deleted, or restore APIs |
Use physical deletion, foreign-key constraints, and the published follow-up migrations. Handle historical soft-deleted data first; do not edit old revisions or bypass guards. |
| Integer settings versions or deleting version records on reset | Apply the existing version-token migration. Conditional update/reset uses the queried UUID/null expected version; reset retains a new version record, and old tokens cannot be reused. |
| Manual aggregate-event collection or private repository operations | Use public SessionProvider.operation(..., aggregate=...); collect events once after successful staging and retain explicit DTO/aggregate/ORM mapping. |
| Earlier conventional HTTP action paths | Check the HTTP module and generated OpenAPI. CRUD routes omit the action segment and static routes take priority; old URL aliases are not retained. |
ReferenceOrderView or public fail_after_add |
Use ReferenceOrderDto and remove the fault-injection parameter. The reference application's reference_orders_0002 migration adds version to existing data; updates compete on the expected version. No old type alias remains. |
Interpreters or backports outside requires-python |
Select a supported interpreter from package metadata, relock, and run application validation. |
Do not remove durable job payload versions merely because their Python APIs were removed. First drain the queue or explicitly migrate its data. See transaction and persistence boundaries.
For maintainers: publishing a changed version
Routine code and documentation commits may retain the current version. Pull requests to main run build, static checks, and short architecture/typing/release-intent checks with read-only permissions. Pushes to main run the full release gates, including locked, lowest-direct, and highest allowed dependency samples, but skip publication unless the version increased across the push; initial creation of main publishes its current version. Manual workflow runs validate only. A failed run may be retried, but a later same-version push does not resume publication: changed contents require another version bump. Follow release operations for publisher setup and retry boundaries. Dependency samples and current API/type checks do not establish backward compatibility or a cross-version upgrade guarantee.
Documentation
The engineering guides below are maintained in Chinese in the private GitHub repository and require repository access. This README and the documents bundled with generated applications provide the English product and development guides.
| If you need to… | Read |
|---|---|
| Implement services, configure modules, or operate the database | Development and operations |
| Understand ownership, lifecycle, transaction, and failure guarantees | Architecture and contracts |
| Understand why a design was chosen | Architecture decision records |
| Check release evidence and remaining validation limits | Status and validation |
| Change the framework itself | Contributor guidance and focused reading paths |
Generated applications include a self-contained English README, AGENTS.md, architecture document, and development guide for their template version. They require an owner/layer/directory/API check before implementation and a review of the seven design principles, DDD boundaries, and actual verification before delivery. The development guide provides CLI entry points, installed-artifact/source comparison, capability recipes, and a complete native HTTP generator example; architecture owns streaming/access boundaries, and the generated README owns configuration migration and shutdown operations. These instructions do not imply an automated architecture gate. Each DDD module README records the empty skeleton and guides the first authorized use case; the independent order example owns its own business documentation and acceptance source. The application maintains these documents after generation; upgrades do not overwrite them. Existing applications can use their installed package's developer_kit/templates as a read-only reference while preserving application rules and decisions. A user-provided framework checkout is also read-only; matching version numbers alone do not prove it matches the consumed artifact.
License
Python DDD Framework is distributed under the hank-repo Proprietary License. Use requires a separate written agreement; public package availability does not grant additional rights. The source repository is private, with public PyPI packages as the intended delivery channel. PgQueuer-derived code retains its upstream license.
Consumer layout migration (0.4)
Move an existing application's Python project files, configuration, source, tests, lockfile, vendor wheels, and deployment files into backend/; keep application documentation and consumer-owned scripts/ at the root. Recreate .venv with uv sync --project backend instead of moving an environment. Update relative script/deployment paths and module README links. Keep portable Python declarations in backend/.python-version and backend/pyproject.toml. Use installed pddd from any application directory, or uv run pddd inside backend/. Module aliases remain standard entry points and runtime types remain AppModule; template selection adds no registration or compatibility path.
Metadata
Release files for python-ddd-framework 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| python_ddd_framework-0.7.0.tar.gz | 445.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| python_ddd_framework-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.1 MB
Release files / python_ddd_framework-0.7.0.tar.gz
| Download URL | python_ddd_framework-0.7.0.tar.gz |
|---|---|
| Size | 445.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a31e374b47dbaa23b5f95e7332c7e38f4acc8cdb1694aa4e42ea98515683a4c
|
|
BLAKE2b-256 checksum How to use checksums |
227e8fd8a1b2129ba5841cb8137be96ef44c565816fc6e779d99e80581cd6deb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / python_ddd_framework-0.7.0-py3-none-any.whl
| Download URL | python_ddd_framework-0.7.0-py3-none-any.whl |
|---|---|
| Size | 668.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8fc71a39f234e9c4daaedcaff1c9a19de7c32bd64c2a0dc11617bd0f4deccf42
|
|
BLAKE2b-256 checksum How to use checksums |
f36704791bf5ff2dd432b88b70846014ed4e2e75f454b8ad1a1e8bcd97d0187b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|