Service Architecture & Composition Documentation¶
1. Architectural Overview¶
The core architecture is built on the Service-Oriented Orchestration pattern. It separates raw data access (Stores) from business logic (Services). This design is optimized for Rust’s ownership model and the requirement for high-integrity, transactional business operations.
Core Principles¶
- Request-Scoped Lifetime: Services are instantiated on a per-request basis. This ensures that every request is isolated, preventing state leakage and ensuring a fresh
CoreCtx(User context, Tracing, etc.) is always used. - Atomic Store Sharing: All services share an
Arc<StoreManager<D>>. This allows every service to access the same database connection pool and, critically, the same transaction context. - Logic Centralization: Business invariants (e.g., "An account must be verified to perform X") live exclusively in the Service layer, never in the Store or Controller.
2. Service Composition Patterns¶
Vertical Composition (Hydration)¶
This pattern is used to transform flat database rows into rich, relational Core models.
- Mechanism: A Service uses its internal Store to fetch a record, then calls other Services to fetch related entities.
- Example:
MembershipServicecallsAccountServiceandWorkspaceServiceto turnaccount_idandworkspace_idinto full domain objects.
Horizontal Orchestration (Cross-Domain)¶
Used for complex workflows that involve multiple distinct domains.
- Mechanism: An orchestration method coordinates multiple service calls within a single transaction.
- Example: A "Workspace Onboarding" flow that creates an Account, then a Workspace, then a Membership in one atomic operation.
3. Dependency Injection & The Service Factory¶
To manage the creation of request-scoped services, a Service Factory is used. It acts as the "Registry of Dependencies."
The Role of the Factory¶
- Encapsulation: It hides the complexity of service construction (e.g., cloning
Arcpointers). - Service-to-Service Injection: It allows a service to be created with its required dependencies already satisfied (e.g., injecting
AccountServiceintoAuthService). - Lazy Creation: Only the services required for the specific request path are instantiated.
4. Circular Dependencies & Statelessness¶
In this architecture, circular dependencies (e.g., Service A needing Service B, while Service B needs Service A) are not an issue.
- On-Demand Instantiation: Because services are lightweight "logic wrappers" holding
Arcpointers to stores, they can be instantiated "on the fly" inside method calls. - Stack Allocation: Child services are typically created on the stack during a method call and dropped immediately after. There are no long-lived heap-allocated reference loops.
5. Transactional Integrity (DbExecutor)¶
Maintaining ACID properties across multiple service calls is handled by the Database Executor (D) carried within the CoreCtx.
The Transaction Flow¶
- A transaction is initiated (typically at the Service Orchestrator or Controller level).
- The transaction-bound
CoreCtxis passed to the first Service. - The same
CoreCtxis passed to subsequent Services. - All services execute their SQL against the same transaction.
- If any service returns an
Err, the transaction is rolled back globally.
6. Service Implementation Standard¶
Every core service should follow this structural template:
- Constructor: A
newmethod that acceptsArc<StoreManager<D>>and any required child Services. - Validator: Integration with
AuthValidatorviaCoreCtxto enforce RBAC/ACL at the start of every business logic method. - Scoping: Explicitly handling
workspace_idoraccount_idfiltering to ensure data isolation. - Transformation: Using
.into()or explicit mapping functions to convert Store rows into Core models.
// Standard Service Signature
pub struct ExampleService<D: DbExecutor> {
sm: Arc<StoreManager<D>>,
other_svc: OtherService<D>, // Injected dependency
}
7. Dependency Injection Strategy: Injection vs. Location¶
The system utilizes Constructor Injection rather than the Service Locator pattern.
Decision: Specificity over Uniformity¶
While passing the ServiceFactory to all services would provide a uniform interface, we opt to pass specific dependencies (StoreManager, CacheManager, and specific child Services) for the following reasons:
- Explicit Dependencies: Struct definitions clearly state what a service requires, improving code readability and maintainability.
- Testability: Services can be unit-tested in isolation by providing mock implementations of specific dependencies rather than a full factory.
- Reduced Coupling: Services remain focused on their specific domain and its immediate neighbors, preventing the "God Object" anti-pattern where every service knows about every other service.
Refined Service Signature¶
pub struct MembershipService<D: DbExecutor> {
sm: Arc<StoreManager<D>>,
account_svc: AccountService<D>,
workspace_svc: WorkspaceService<D>,
}
8. Service Trait Architecture¶
The system uses a decomposed trait pattern to define service capabilities. This ensures type safety and prevents "interface bloat."
Capability-Based Design¶
Instead of a single "God Trait" for CRUD, we split responsibilities into specific capability traits:
- CoreModelCreateService: Handles entity instantiation and persistence.
- CoreModelDescribeService: Handles single-entity retrieval and hydration.
- CoreModelListService: Handles collection queries, filtering, and pagination.
- CoreModelUpdateService: Handles partial or full entity mutations.
- CoreModelDeleteService: Handles entity removal (logical or physical).
Benefits¶
- Contract Honesty: A service like
TokenServicecan implementCreateandDescribewithout being forced to implement a non-existentUpdatemethod. - Generic Bound Granularity: Utility functions can require only the specific capability they need (e.g.,
where S: CoreModelListService). - Consistent Naming: All capability traits follow the
CoreModel[Action]Serviceconvention for high discoverability.