-
-
Notifications
You must be signed in to change notification settings - Fork 0
ADR 007 specification pattern
Bharat Joshi edited this page Apr 30, 2026
·
1 revision
Status: Accepted
Date: 2026-04-26
Authors: Spectrayan Team
Domain aggregates (Prompt, Workflow) enforce business rules on state transitions — e.g., a prompt can only be submitted for review if it's in DRAFT status and has at least one version. Initially, these checks were scattered as inline if statements inside aggregate methods, making them hard to test, compose, or reuse.
We needed a pattern that:
- Makes business rules explicit, named, and testable in isolation
- Provides clear error messages when a rule is violated
- Supports composition (AND/OR of multiple rules)
Implement the Specification pattern with a generic Specification<T> interface in the shared domain layer.
classDiagram
class Specification~T~ {
<<interface>>
+isSatisfiedBy(T candidate) boolean
+unsatisfiedReason(T candidate) String
+and(Specification~T~ other) Specification~T~
+or(Specification~T~ other) Specification~T~
}
class PromptSpecifications {
+isEditable()$ Specification~Prompt~
+isSubmittable()$ Specification~Prompt~
+isDeletable()$ Specification~Prompt~
+isRollbackable()$ Specification~Prompt~
}
class WorkflowSpecifications {
+isApprovable()$ Specification~Workflow~
+isRejectable()$ Specification~Workflow~
+isCancellable()$ Specification~Workflow~
}
class Prompt {
-status: PromptStatus
-currentVersion: int
+createNewVersion()
+submitForReview()
+assertDeletable()
}
class Workflow {
-status: WorkflowStatus
-currentStep: int
+approveCurrentStep()
+rejectCurrentStep()
}
Specification~T~ <|.. PromptSpecifications : creates
Specification~T~ <|.. WorkflowSpecifications : creates
Prompt ..> PromptSpecifications : uses
Workflow ..> WorkflowSpecifications : uses
Aggregates delegate rule checking to specifications via a private assertSatisfies method:
public void submitForReview() {
assertSatisfies(PromptSpecifications.isSubmittable(),
"Cannot submit for review");
this.status = PromptStatus.IN_REVIEW;
}
// Aggregates also expose read-only query methods:
public boolean isEditable() {
return PromptSpecifications.isEditable().isSatisfiedBy(this);
}| Aggregate | Specification | Rule |
|---|---|---|
Prompt |
isEditable() |
Status is NOT IN_REVIEW
|
Prompt |
isSubmittable() |
Status is DRAFT AND currentVersion >= 1
|
Prompt |
isDeletable() |
Status is NOT IN_REVIEW AND NOT APPROVED
|
Prompt |
isRollbackable() |
currentVersion > 1 |
Workflow |
isApprovable() |
Status is PENDING AND has pending steps |
Workflow |
isRejectable() |
Status is PENDING AND has pending steps |
Workflow |
isCancellable() |
Status is NOT in a terminal state |
| Approach | Testability | Reusability | Error Messages |
|---|---|---|---|
Inline if statements |
❌ Coupled to aggregate | ❌ Duplicated | ❌ Ad-hoc strings |
| Specification pattern | ✅ Unit-testable independently | ✅ Composable via and()/or()
|
✅ Structured unsatisfiedReason()
|
- Business rules are named, discoverable, and testable without instantiating full aggregates
-
unsatisfiedReason()produces clear API error messages for clients - Specifications can be composed:
isEditable().and(isOwnedBy(userId)) - Query methods (
isEditable(),isDeletable()) enable UI to pre-check actions
- Additional abstraction layer — simple rules may feel over-engineered (mitigated by consistency)
- Specification classes must be kept in sync with aggregate state fields
- Getting Started — For Teams
- Platform Overview
- Dashboard
- Prompt Registry
- Workflows & Approvals
- Security & Guardrails
- Architecture Overview
- ADR 001: Hybrid State Management
- ADR 002: Project RBAC Model
- ADR 003: Hexagonal Naming Conventions
- ADR 004: Spring Modulith Boundaries
- ADR 005: System Prompt Administration
- ADR 006: Contract First API Design
- ADR 007: Specification Pattern
- ADR 008: Reactive Persistence
- ADR 009: SSE Notifications
- Backend Workflows