Command Query Responsibility Segregation (CQRS) ¶
CQRS is a fancy term for a simple idea: Separate the code that changes data from the code that reads data.
In Stario, this pattern is the "secret sauce" for building multiplayer, real-time apps with zero sync logic.
The Mental Model ¶
- Commands (Writes): User wants to do something. Use
POST. Returns204 No Content. - Queries (Reads): User wants to see something. Use a persistent SSE connection. Returns HTML patches.
The Pattern in 3 Steps ¶
- Establish a Query Stream: The client opens an SSE connection on page load (
data.init). - Fire a Command: User clicks a button, sending a
POSTrequest. The server updates the database and returns nothing. - Notify and Sync: The server uses a
Relayto notify the Query Stream. The stream re-renders the UI and pushes the patch.
Example: Live Task List ¶
relay = Relay()
tasks = []
# 1. The Query (Persistent)
async def task_stream(c: Context, w: Writer):
# Push initial state
w.patch(Ul({"id": "list"}, *[Li(t) for t in tasks]))
# Wait for notifications
async for _ in w.alive(relay.subscribe("tasks")):
w.patch(Ul({"id": "list"}, *[Li(t) for t in tasks]))
# 2. The Command (Stateless)
async def add_task(c: Context, w: Writer):
signals = await c.signals()
tasks.append(signals["new_task"])
# 3. Notify
relay.publish("tasks", None)
w.empty()
Why This Wins ¶
- Multiplayer by Default: Any number of users can subscribe to the same Relay topic. When one user fires a command, everyone's Query Stream re-renders.
- No Cache Invalidation: The server just sends the new truth. The client has no local state to get "stale."
- Fire and Forget: Commands are lightning-fast because they don't have to wait for a complex UI response to be generated.
Honest Assessment ¶
CQRS is overkill for a simple "Contact Us" form, but it is essential for anything collaborative (Chat, Dashboards, Real-time Games). If multiple people are looking at the same data, use CQRS.
