ADR-009: Domain Service vs Application Service Classification¶
Status¶
Approved - Implemented
Context¶
The SyRF.ProjectManagement.Core library historically contained both domain logic and application-layer orchestration in the same project. This blurred the DDD boundary between services that encode business rules and services that merely coordinate data access. During the refactoring of StageReviewService (PR #2543), we needed explicit rules to determine what belongs in the domain layer vs the application layer, and where each should live in the project structure.
Decision¶
Classification Rules¶
A service is a Domain Service if it meets ANY of these criteria:
- Domain Expert Recognition: A domain expert would recognise the operation as part of their workflow, not just as "getting data"
- Ordering Invariants: Removing or reordering steps would violate a domain invariant or produce incorrect business behaviour
- Business Decisions: The service makes decisions based on domain rules (not just routing data between components)
- Cross-Aggregate Logic: The logic spans multiple aggregates but doesn't naturally belong to any single entity
A service is an Application Service if ALL of these are true:
- No Domain Decisions: The service makes no business decisions — it loads, delegates, and returns
- No Ordering Constraints: Steps could be reordered or removed without violating domain rules
- Substitutable by Caller: The controller could call the repository directly without losing any business logic
- Infrastructure Coordination: Its purpose is to bridge between external concerns (HTTP, messaging) and the domain
The Grey Area Test¶
When a service is ambiguous, ask: "If I inlined this service into the controller, would I be putting domain logic in the controller?"
- If yes → it's a domain service (the sequencing/validation IS the domain logic)
- If no → it's an application service (the controller could do this directly)
Project Location¶
| Layer | Project | Contains |
|---|---|---|
| Domain | SyRF.ProjectManagement.Core |
Entities, Value Objects, Domain Services, Domain Events, Repository interfaces |
| Application | SyRF.ProjectManagement.Application |
Application Services that orchestrate domain operations without encoding business rules |
| Infrastructure | SyRF.ProjectManagement.Mongo.Data |
Repository implementations, database-specific code |
The Application project: - References Core (follows the dependency rule: application → domain) - Is referenced by API Endpoint and PM Endpoint (hosts depend on application layer) - Does NOT reference infrastructure (Mongo.Data) — it uses repository interfaces from Core
Classification of Review Services¶
Applying these rules to the study review domain:
| Service | Classification | Reason | Project |
|---|---|---|---|
StudyAssignmentPolicy |
Domain Service | Pure business rules for study assignment. No I/O. Domain expert would recognise the decision matrix. | Core |
StageReviewService |
Domain Service (workflow) | GetRandomStudyAsync encodes domain-meaningful sequencing: validate project state → validate stage → determine assignment → fetch study. Inlining into controller would put domain validation in the presentation layer. |
Core |
ReviewStatsQueryService |
Application Service | All methods are load-delegate-return with no domain decisions. Controller could call the repository directly. | Application |
Stage.HasReachedMaxInProgress |
Entity Behaviour | Domain logic on the entity that owns the configuration (MaxInProgress, HideExcludedStudiesFromReviewers). | Core (on entity) |
Value Objects (always Domain)¶
StudyAssignment, StudyFetchStrategy, ReviewStatus, StudyReviewStatus, PoolSwitchStatus, ReviewStatusInfo — all domain vocabulary, remain in Core.
Consequences¶
Positive¶
- Compile-time boundary enforcement: Application services cannot accidentally depend on infrastructure, and domain code cannot depend on application concerns
- Clear placement rules: New services can be classified using the documented criteria
- Testability: Domain services tested without infrastructure mocks; application services tested with simple mock setups
- Discoverability: Developers know where to find business logic (Core) vs orchestration (Application)
Negative¶
- Additional project: One more
.csprojin the solution increases build graph complexity slightly - Migration effort: Existing application services in Core would need to be migrated over time (not all at once)
Neutral¶
- The rules are guidelines, not absolutes. Edge cases should be discussed and the decision documented
- Services can be reclassified if their responsibilities change (e.g., if orchestration gains domain logic, promote to domain service)
References¶
- Evans, Eric. Domain-Driven Design, Chapter 5: "Services"
- Vernon, Vaughn. Implementing Domain-Driven Design, Chapter 7: "Application Services"
- PR #2543: Initial implementation of this separation