Hexagonal Architecture & Dependency Injection
Relevant source files
The following files were used as context for generating this wiki page:
Forze is a Python library designed to implement Domain-Driven Design (DDD) and Hexagonal Architecture. The core of this system is an explicit dependency injection (DI) engine that separates business logic (the "Domain") from infrastructure concerns (the "Adapters"). By using ports—abstract interfaces—and a robust DepsRegistry, the framework ensures that dependencies flow strictly inward toward the domain layer.
This architecture enables components to be tested in isolation, infrastructure to be swapped with minimal impact on logic, and provides a clear structure for backend services. The system revolves around three primary execution plans: the DepsRegistry for dependency management, the LifecyclePlan for resource management, and the OperationRegistry for handling application logic.
Sources: README.md, skills/forze-wiring/SKILL.md
Layered Architecture Principles
The project enforces a strict layered structure where dependencies only flow inward. This prevents the domain logic from being coupled to specific databases, message brokers, or web frameworks.
| Layer | Responsibility | Constraints |
|---|---|---|
| Domain | Pure business logic, entities, and validation. | No external dependencies or infrastructure imports. |
| Application | Handlers, contracts (ports), and composition logic. | Imports only the Domain layer. |
| Infrastructure | Adapters implementing application contracts (e.g., Postgres, Redis). | Implements ports; never imported by Domain or Application handlers. |
| Interface | Entry points like HTTP (FastAPI) or WebSockets. | Resolves handlers via the frozen DepsRegistry. |
Sources: skills/forze-framework-usage/SKILL.md
Dependency Injection Core
The DI system in Forze is managed by the DepsRegistry and ExecutionContext. It uses a "routed" dependency model, allowing for multiple implementations of the same interface (Port) to coexist, distinguished by a logical name or "route".
Registry and Containers
The Deps container is the fundamental unit for registering providers. It supports two primary modes:
- Plain: A single provider per
DepKey(e.g., a shared database client). - Routed: Multiple providers per
DepKey, mapped to specific logical names (e.g., differentDocumentSpecroutes).
The diagram above illustrates the transition from the configuration-time registry to the runtime execution context.
Sources: skills/forze-deps-consumption/SKILL.md, skills/forze-wiring/SKILL.md
ExecutionContext Resolution
At runtime, the ExecutionContext acts as the service locator for resolved ports. Handlers resolve infrastructure by logical specification names (e.g., spec.name).
| Helper Method | Port Returned | Logical Spec Used |
|---|---|---|
ctx.document.query(spec) | DocumentQueryPort | DocumentSpec |
ctx.cache(spec) | CachePort | CacheSpec |
ctx.storage.command(spec) | StorageCommandPort | StorageSpec |
ctx.tx_ctx.scope(route) | AsyncContextManager | Transaction Route (e.g., "default") |
Sources: skills/forze-framework-usage/SKILL.md, skills/forze-wiring/SKILL.md
Hexagonal Ports and Adapters
In Forze, a "Port" is a protocol defining a capability (e.g., querying a document), while an "Adapter" is the infrastructure-specific implementation (e.g., a Postgres adapter).
The Port Resolution Flow
Application handlers define what they need through Port protocols. The DI system wires the appropriate Adapter at runtime based on the DepsRegistry configuration.
Sequence showing how a handler obtains a port from the context without knowing the underlying adapter implementation.
Sources: skills/forze-framework-usage/SKILL.md, skills/forze-deps-consumption/SKILL.md
Integration and Lifecycle Wiring
Dependency injection is closely tied to the resource lifecycle. While DepsModule instances define what is available, LifecyclePlan steps manage when connections (like database pools) are opened and closed.
DepsModules
DepsModule classes (e.g., PostgresDepsModule) map logical resource names to physical configurations.
__call__method: This method in aDepsModuleshould only register providers/factories into aDepscontainer.- Isolation: Network connections should not be opened during the dependency registration phase.
Lifecycle Steps
Actual resource initialization occurs in LifecycleStep functions. These are organized into a LifecyclePlan that determines the startup and shutdown order based on dependencies between services (e.g., Vault must start before Postgres if Postgres needs Vault secrets).
Sources: skills/forze-wiring/SKILL.md, skills/forze-deps-consumption/SKILL.md
Transactional Scopes
Transactions are managed as a cross-cutting concern within the DI system. The ExecutionContext provides a tx_ctx to manage transaction boundaries.
- Route-based Transactions: Transactions are resolved using the same route names used for dependencies (e.g.,
TxRoute.DEFAULT). - Implicit vs Explicit: Transactions are not implicit. Developers must bind a transaction route to specific operations in the
OperationRegistryor use thectx.tx_ctx.scope(route)context manager in handlers.
Sources: skills/forze-wiring/SKILL.md, skills/forze-framework-usage/SKILL.md
Summary
The Hexagonal Architecture and Dependency Injection systems in Forze provide a rigorous framework for decoupling business logic from infrastructure. By using a routed dependency model and centralizing resolution in the ExecutionContext, Forze allows developers to build complex systems where data stores, caches, and external services are treated as swappable components. The strict separation between the registration of dependencies (DepsRegistry) and the management of their underlying resources (LifecyclePlan) ensures that applications remain stable, testable, and maintainable as they scale.