Command Query Responsibility Segregation (CQRS)¶
Overview¶
VitalBridge adopts the Command Query Responsibility Segregation (CQRS) pattern to separate operations that modify state from operations that retrieve data.
CQRS helps maintain clear service boundaries, simplifies business logic, and aligns naturally with the platform's event-driven architecture.
The core principle is simple:
- Commands change state
- Queries read state
A request should never do both.
Why CQRS?¶
Without CQRS, a service often handles reads and writes through the same model.
flowchart LR
CLIENT["Client"]
SERVICE["Application Service"]
DB[("Database")]
CLIENT --> SERVICE
SERVICE --> DB
As systems grow, read and write concerns evolve differently.
Typical examples include:
Write Operations¶
- Create Tenant
- Add Provider
- Register Patient
- Create Appointment
- Cancel Appointment
- Submit Leave Request
Read Operations¶
- List Providers
- View Schedule
- Search Patients
- View Appointments
- View Availability
CQRS separates these concerns.
CQRS Model¶
flowchart TB
CLIENT["Client Application"]
COMMANDS["Commands"]
QUERIES["Queries"]
CLIENT --> COMMANDS
CLIENT --> QUERIES
Commands and queries have different responsibilities and different rules.
Commands¶
Commands represent business actions.
Commands always attempt to change state.
Examples:
- Create Tenant
- Create Provider
- Create Patient
- Create Appointment
- Cancel Appointment
- Approve Leave Request
Command Flow¶
flowchart LR
CLIENT["Client"]
COMMAND["Create Appointment"]
SERVICE["Appointment Service"]
DB[("Appointment DB")]
OUTBOX["Outbox Event"]
CLIENT --> COMMAND
COMMAND --> SERVICE
SERVICE --> DB
SERVICE --> OUTBOX
A command:
- Validates business rules
- Modifies data
- Produces domain events
Queries¶
Queries retrieve information.
Queries never modify state.
Examples:
- Get Appointment
- List Patients
- View Schedule
- View Availability
- Search Providers
Query Flow¶
flowchart LR
CLIENT["Client"]
QUERY["Get Available Slots"]
SERVICE["Schedule Service"]
DB[("Schedule DB")]
CLIENT --> QUERY
QUERY --> SERVICE
SERVICE --> DB
A query:
- Reads data
- Returns data
- Does not publish events
- Does not modify state
Command Example¶
Patient books an appointment.
sequenceDiagram
participant Patient
participant AppointmentService
participant AppointmentDB
participant Outbox
Patient->>AppointmentService: Create Appointment
AppointmentService->>AppointmentDB: Save Appointment
AppointmentService->>Outbox: Create appointment.created
AppointmentService-->>Patient: Success
The command changes business state.
Query Example¶
Patient views upcoming appointments.
sequenceDiagram
participant Patient
participant AppointmentService
participant AppointmentDB
Patient->>AppointmentService: Get Upcoming Appointments
AppointmentService->>AppointmentDB: Read Appointments
AppointmentDB-->>AppointmentService: Results
AppointmentService-->>Patient: Appointment List
The query only reads information.
CQRS and Events¶
Commands often produce events.
Queries never produce events.
flowchart LR
COMMAND["Command"]
DB[("Database")]
EVENT["Domain Event"]
COMMAND --> DB
DB --> EVENT
Examples:
| Command | Event |
|---|---|
| Create Tenant | tenant.created |
| Create Provider | provider.created |
| Create Patient | patient.created |
| Create Appointment | appointment.created |
| Cancel Appointment | appointment.cancelled |
This integrates naturally with VitalBridge's event-driven architecture.
Service Ownership¶
Commands must always be sent to the service that owns the business domain.
flowchart LR
CLIENT["Client"]
APPT["Appointment Service"]
APPT_DB[("Appointment DB")]
CLIENT --> APPT
APPT --> APPT_DB
For example:
- Appointment commands belong to Appointment Service
- Schedule commands belong to Doctor Schedule Service
- Patient commands belong to Patient Service
Services may not execute commands against another service's database.
Cross-Service Queries¶
Sometimes a service requires information owned by another service.
flowchart LR
APPT["Appointment Service"]
DOC["Doctor Service"]
APPT -->|"Query"| DOC
In VitalBridge:
- Commands are asynchronous through events
- Queries are synchronous through request/response communication
This preserves ownership boundaries while allowing services to retrieve information when needed.
What CQRS Is Not¶
CQRS is often confused with Event Sourcing.
VitalBridge does not require:
- Event Sourcing
- Event replay as the source of truth
- Separate read databases
- Separate deployment units for reads and writes
The platform simply separates:
- State-changing operations
- Read-only operations
This lightweight interpretation provides most of the benefits without unnecessary complexity.
Benefits¶
CQRS provides several advantages:
Clear Responsibility¶
Every request is either:
- A command
- A query
Never both.
Better Maintainability¶
Business logic remains focused and easier to reason about.
Better Scalability¶
Read and write workloads can evolve independently.
Natural Event Integration¶
Commands produce domain events that integrate seamlessly with the event-driven architecture.
Strong Service Boundaries¶
Only owning services may execute business commands.
Relationship to Other Architectural Patterns¶
CQRS works alongside several other platform patterns.
flowchart TB
CQRS["CQRS"]
OUTBOX["Transactional Outbox"]
EVENTS["Event-Driven Architecture"]
TENANT["Multi-Tenancy"]
CQRS --> OUTBOX
OUTBOX --> EVENTS
EVENTS --> TENANT
Together these patterns form the foundation of the VitalBridge architecture.
Architectural Rules¶
VitalBridge enforces the following CQRS rules:
Commands¶
- Change state
- Validate business rules
- Generate events
- Execute only within owning services
Queries¶
- Read state
- Never change data
- Never publish events
- May retrieve information from other services
Forbidden¶
- Commands that return complex read models
- Queries that modify state
- Cross-service database writes
- Mixed command/query operations
Following these rules keeps the platform predictable, scalable, and aligned with its event-driven architecture.