Command Query Responsibility Segregation (CQRS)

CQRS separates read operations (queries) from write operations (commands). Instead of a single model serving both purposes, you have two: one optimized for fetching state, one for changing it.

That's it. The idea is simple. How you implement it can be as simple or complex as you want.

The REST Problem

REST HTTP methods seem to cleanly map operations to verbs, but they don't. Real systems break this down immediately:

  • Soft deletes: Is it DELETE or PATCH?
  • Bulk operations: Is queuing work POST, or something else?
  • Long-running processes: Does POST vs PUT matter when it's async?
  • Complex queries: GET with filters, arrays, nested objects - when does this become POST?

The method doesn't matter as much as the mental model: Is the user asking for data, or asking me to do something?

Stario's philosophy: stop overthinking it. Use this pattern to get started, discover real patterns as you go, and refactor later when you have concrete constraints.

The Pattern: Persistent Queries + Stateless Commands

Consider the following approach:

  1. Every user action is a POST to a command endpoint. Commands don't return data - they return 204 No Content (this is what detached_command was built for).
  2. On page load, establish a long-lived query connection (SSE) to /updates or similar.
  3. On the backend, wire commands to a queue that notifies the persistent query connection of state changes.
# Command: user does something
@app.detached_command("/add_todo")
async def add_todo(text: Signal[str]):
    # Command input is validated and actual execution happens in the background
    todos.append({"text": text, "done": False})
    notify_state_changed()  # Queue handles this

# Query: establish persistent connection
@app.query("/updates")
async def updates_stream():
    """Persistent SSE connection"""
    while True:
        state_changed.clear()
        yield render_todos(todos)  # Send complete state
        await state_changed.wait()

Data Flow:

  • User clicks button → POST /add_todo
  • Command (segregated responsibility): validates command input, returns 204 immediately.
  • User's browser continues (doesn't wait)
  • Command modifies state and notifies the queue.
  • Queue wakes up the /updates connection and sends the new state to the browser.
  • Query (segregated responsibility): reads complete state, renders HTML
  • Query yields new HTML through SSE
  • Browser receives and displays update

Notice: Commands never return state. Queries never modify state. The queue is the only bridge. That's all there is to segregation.

When a command finishes, it doesn't need to return the new state. The command just notifies the queue. The persistent query connection sees that notification, re-renders, and streams the new state back to the user.

Why This Works

No request/response mismatch: Commands are fire-and-forget (204). State updates arrive through the persistent connection - the only channel specifically designed for streaming updates.

Multiplayer for free: All connected clients have the same SSE stream open. When state changes, everyone sees it immediately. No sync logic needed.

Rendering is centralized: The render function receives complete state and generates complete HTML. When the UI is wrong, you know exactly where to fix it.

Technical Notes

These operations are remarkably cheap and simple:

  • HTTP/2 or HTTP/3 makes multiple concurrent connections and streaming efficient.
  • SSE connections are essentially free - they're just long-lived HTTP responses.
  • Brotli compression compresses SSE streams exceptionally well. Repeated HTML structures compress dramatically.
  • No proxy headaches: Unlike WebSockets, SSE is just HTTP. Proxies, load balancers, and CDNs all understand it. No special configuration needed.

You get WebSocket-like interactivity without the operational complexity or network restrictions. The tradeoff is inherent to the model: the server sends updates, the client receives them. No true bidirectional streams. For most applications, this is exactly what you want.