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. Returns 204 No Content.
  • Queries (Reads): User wants to see something. Use a persistent SSE connection. Returns HTML patches.

The Pattern in 3 Steps

  1. Establish a Query Stream: The client opens an SSE connection on page load (data.init).
  2. Fire a Command: User clicks a button, sending a POST request. The server updates the database and returns nothing.
  3. Notify and Sync: The server uses a Relay to 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.