DEV Community

turboline-ai
turboline-ai

Posted on

Your "Simple" CRUD App Is Secretly a Distributed Systems Problem

Your "Simple" CRUD App Is Secretly a Distributed Systems Problem

Every developer has a project they underestimated. Mine was a scoreboard.

Not a financial trading platform. Not a multiplayer game with thousands of concurrent users. A scoreboard. For a local basketball league. The kind of thing you'd expect to knock out in a weekend with a database, a few API endpoints, and maybe a sprinkle of WebSockets to make it feel live.

Three weeks later, I was deep in the weeds of conflict resolution, idempotency keys, and connection state management. Here's what that experience taught me about the class of problems that hides behind deceptively simple state synchronization requirements.

The Illusion of Simplicity

A score is just a number. A number that goes up. How hard can it be?

The trouble starts the moment you introduce more than one actor. A courtside scorekeeper updates the score on a tablet. A wall display needs to reflect that instantly. A coach's phone is also showing the score. So is the league's website. Now you have a distributed state problem, and all the classic distributed systems dragons show up: ordering, consistency, and fault tolerance.

The naive implementation treats this like a polling problem. Every client calls GET /match/:id every few seconds. This works until it doesn't. You get stale reads, thundering herd on score updates, and the wall display lagging a full polling interval behind reality during the most exciting moments of the game.

Optimistic Locking Is Not Optional

Let's say you upgrade to a proper event-driven model. The scorekeeper's tablet sends a PATCH /match/:id/score request. Simple enough. Except what happens when the network hiccups and the request fires twice? Or two scorekeepers are both entering corrections simultaneously?

Without optimistic locking, you get phantom increments. A score of 47 becomes 49 when it should be 48. In a financial system, this gets you fired. In a basketball game, it gets you screamed at by a coach.

The fix is to include a version token with every write. The server rejects any update that doesn't match the current version, forcing the client to re-fetch and retry. It looks something like this:

def update_score(match_id, new_score, expected_version):
    response = table.update_item(
        Key={"match_id": match_id},
        UpdateExpression="SET score = :new_score, version = :new_version",
        ConditionExpression="version = :expected_version",
        ExpressionAttributeValues={
            ":new_score": new_score,
            ":new_version": expected_version + 1,
            ":expected_version": expected_version,
        },
        ReturnValues="ALL_NEW"
    )
    return response["Attributes"]
Enter fullscreen mode Exit fullscreen mode

DynamoDB's conditional writes make this pattern straightforward. If the condition fails, you catch the ConditionalCheckFailedException, re-read the current state, and let the client decide what to do next. The database becomes the source of truth, not the most recent HTTP request.

The Change Propagation Problem

Winning the write conflict battle is only half the work. You still need to get the updated state to every connected client, reliably and quickly.

This is where DynamoDB Streams earns its place in the architecture. Every confirmed write to the match table emits a stream record. A Lambda function or a persistent consumer reads that stream and broadcasts the new state over WebSockets to every connected spectator. The database mutation and the broadcast are decoupled: the write doesn't block on fan-out, and a slow or disconnected client doesn't pollute the write path.

The elegant part of this pattern is that it handles eventual consistency gracefully. Clients don't receive individual delta events that they have to reassemble in order. They receive the full current state on every update. Late-joining clients, reconnecting phones, and wall displays that briefly lost Wi-Fi all converge to the same state because they're always receiving snapshots, not patches.

The less elegant part is managing connection state. Which client IDs are currently connected? What do you do when a WebSocket connection drops between a write and the broadcast? You need a connection registry, and that registry itself becomes a consistency surface you have to think about.

Rate Limiting Is More Interesting Than It Sounds

Sports scoring is low-frequency by nature. A basketball game might produce 150-200 scoring events over two hours. That's not a load problem. But rate limiting still matters here, just for different reasons.

Without it, a misbehaving client can hammer the score endpoint with rapid increments. A bug in the tablet app during a fast break could fire ten requests in two seconds. Rate limiting at the endpoint level, combined with idempotency keys on the client side, gives you defense in depth. The idempotency key ensures that even if a client retries a failed request, the server recognizes it as a duplicate and returns the previous result rather than applying the operation again.

These two mechanisms, rate limiting and idempotency, are what separate a system that works in demos from one that holds up when the network misbehaves or a user's thumb slips.

What This Tells You About "Simple" State Sync

The scoreboard problem is a useful lens because the stakes are low enough that you can reason clearly, but the surface area is large enough to expose real distributed systems complexity. You end up needing optimistic locking, idempotent writes, change data capture, WebSocket fan-out, and connection state management before you've written a single line of frontend code.

Turboline's streaming infrastructure is built around exactly this architecture. The DynamoDB Streams plus WebSocket wiring is the pattern, but at scale you also need backpressure handling, connection lifecycle management, and fan-out that doesn't fall apart when a viral moment sends ten thousand simultaneous spectators to the same feed.

The concrete takeaway is this: any feature that involves more than one client observing shared state is a distributed systems problem. The simplicity of the domain, a sports score, a document status, a ride location, doesn't change that. The right time to recognize that is before you build the polling endpoint, not after your third production incident.

Top comments (0)