244 lines
4.8 KiB
Markdown
244 lines
4.8 KiB
Markdown
CQRS Light with Clean Architecture
|
|
|
|
This document defines CQRS Light as a pragmatic, production-ready approach that integrates cleanly with Clean Architecture.
|
|
|
|
It is intentionally non-dogmatic, avoids event-sourcing overhead, and focuses on clarity, performance, and maintainability.
|
|
|
|
⸻
|
|
|
|
1. What CQRS Light Is
|
|
|
|
CQRS Light separates how the system writes data from how it reads data — without changing the core architecture.
|
|
|
|
Key properties:
|
|
• Commands and Queries are separated logically, not infrastructurally
|
|
• Same database is allowed
|
|
• No event bus required
|
|
• No eventual consistency by default
|
|
|
|
CQRS Light is an optimization, not a foundation.
|
|
|
|
⸻
|
|
|
|
2. What CQRS Light Is NOT
|
|
|
|
CQRS Light explicitly does not include:
|
|
• Event Sourcing
|
|
• Message brokers
|
|
• Projections as a hard requirement
|
|
• Separate databases
|
|
• Microservices
|
|
|
|
Those can be added later if needed.
|
|
|
|
⸻
|
|
|
|
3. Why CQRS Light Exists
|
|
|
|
Without CQRS:
|
|
• Reads are forced through domain aggregates
|
|
• Aggregates grow unnaturally large
|
|
• Reporting logic pollutes the domain
|
|
• Performance degrades due to object loading
|
|
|
|
CQRS Light solves this by allowing:
|
|
• Strict domain logic on writes
|
|
• Flexible, optimized reads
|
|
|
|
⸻
|
|
|
|
4. Core Architectural Principle
|
|
|
|
Writes protect invariants. Reads optimize information access.
|
|
|
|
Therefore:
|
|
• Commands enforce business rules
|
|
• Queries are allowed to be pragmatic and denormalized
|
|
|
|
⸻
|
|
|
|
5. Placement in Clean Architecture
|
|
|
|
CQRS Light does not introduce new layers.
|
|
It reorganizes existing ones.
|
|
|
|
core/
|
|
└── <context>/
|
|
└── application/
|
|
├── commands/ # Write side (Use Cases)
|
|
└── queries/ # Read side (Query Use Cases)
|
|
|
|
Domain remains unchanged.
|
|
|
|
⸻
|
|
|
|
6. Command Side (Write Model)
|
|
|
|
Purpose
|
|
• Modify state
|
|
• Enforce invariants
|
|
• Emit outcomes
|
|
|
|
Characteristics
|
|
• Uses Domain Entities and Value Objects
|
|
• Uses Repositories
|
|
• Uses Output Ports
|
|
• Transactional
|
|
|
|
Example Structure
|
|
|
|
core/racing/application/commands/
|
|
├── CreateLeagueUseCase.ts
|
|
├── ApplyPenaltyUseCase.ts
|
|
└── RegisterForRaceUseCase.ts
|
|
|
|
|
|
⸻
|
|
|
|
7. Query Side (Read Model)
|
|
|
|
Purpose
|
|
• Read state
|
|
• Aggregate data
|
|
• Serve UI efficiently
|
|
|
|
Characteristics
|
|
• No domain entities
|
|
• No invariants
|
|
• No side effects
|
|
• May use SQL/ORM directly
|
|
|
|
Example Structure
|
|
|
|
core/racing/application/queries/
|
|
├── GetLeagueStandingsQuery.ts
|
|
├── GetDashboardOverviewQuery.ts
|
|
└── GetDriverStatsQuery.ts
|
|
|
|
Queries are still Use Cases, just read-only ones.
|
|
|
|
⸻
|
|
|
|
8. Repositories in CQRS Light
|
|
|
|
Write Repositories
|
|
• Used by command use cases
|
|
• Work with domain entities
|
|
• Enforce consistency
|
|
|
|
core/racing/domain/repositories/
|
|
└── LeagueRepositoryPort.ts
|
|
|
|
|
|
⸻
|
|
|
|
Read Repositories
|
|
• Used by query use cases
|
|
• Return flat, optimized data
|
|
• Not domain repositories
|
|
|
|
core/racing/application/ports/
|
|
└── LeagueStandingsReadPort.ts
|
|
|
|
Implementation lives in adapters.
|
|
|
|
⸻
|
|
|
|
9. Performance Benefits (Why It Matters)
|
|
|
|
Without CQRS Light
|
|
• Aggregate loading
|
|
• N+1 queries
|
|
• Heavy object graphs
|
|
• CPU and memory overhead
|
|
|
|
With CQRS Light
|
|
• Single optimized queries
|
|
• Minimal data transfer
|
|
• Database does aggregation
|
|
• Lower memory footprint
|
|
|
|
This results in:
|
|
• Faster endpoints
|
|
• Simpler code
|
|
• Easier scaling
|
|
|
|
⸻
|
|
|
|
10. Testing Strategy
|
|
|
|
Commands
|
|
• Unit tests
|
|
• Mock repositories and ports
|
|
• Verify output port calls
|
|
|
|
Queries
|
|
• Simple unit tests
|
|
• Input → Output verification
|
|
• No mocks beyond data source
|
|
|
|
CQRS Light reduces the need for complex integration tests.
|
|
|
|
⸻
|
|
|
|
11. When CQRS Light Is a Good Fit
|
|
|
|
Use CQRS Light when:
|
|
• Read complexity is high
|
|
• Write logic must stay strict
|
|
• Dashboards or analytics exist
|
|
• Multiple clients consume the system
|
|
|
|
Avoid CQRS Light when:
|
|
• Application is CRUD-only
|
|
• Data volume is small
|
|
• Read/write patterns are identical
|
|
|
|
⸻
|
|
|
|
12. Migration Path
|
|
|
|
CQRS Light allows incremental adoption:
|
|
1. Start with classic Clean Architecture
|
|
2. Separate commands and queries logically
|
|
3. Optimize read paths as needed
|
|
4. Introduce events or projections later (optional)
|
|
|
|
No rewrites required.
|
|
|
|
⸻
|
|
|
|
13. Key Rules (Non-Negotiable)
|
|
• Commands MUST use the domain
|
|
• Queries MUST NOT modify state
|
|
• Queries MUST NOT enforce invariants
|
|
• Domain MUST NOT depend on queries
|
|
• Core remains framework-agnostic
|
|
|
|
⸻
|
|
|
|
14. Mental Model
|
|
|
|
Think of CQRS Light as:
|
|
|
|
One core truth, two access patterns.
|
|
|
|
The domain defines truth.
|
|
Queries define convenience.
|
|
|
|
⸻
|
|
|
|
15. Final Summary
|
|
|
|
CQRS Light provides:
|
|
• Cleaner domain models
|
|
• Faster reads
|
|
• Reduced complexity
|
|
• Future scalability
|
|
|
|
Without:
|
|
• Infrastructure overhead
|
|
• Event sourcing complexity
|
|
• Premature optimization
|
|
|
|
It is the safest way to gain CQRS benefits while staying true to Clean Architecture. |