Rails API design

Optimistic locking for the /articles endpoint

Prevent stale CMS saves without turning article editing into a blocking workflow.

In a CMS, multiple editors can open the same article, make changes, and save at different times. Without a concurrency-control strategy, the last save can silently overwrite earlier work. For an article workflow, that is usually the wrong failure mode: the system appears to accept a change, but editorial content is lost.

The right default for the /articles endpoint is optimistic locking. Rails already supports this through Active Record, using a version column such as lock_version. Rails checks whether a record has changed since it was loaded and raises ActiveRecord::StaleObjectError when a stale update is attempted. The Rails API documentation describes this as allowing multiple users to edit the same record while detecting whether another process changed it before the update is applied.1

Core idea: article editing remains non-blocking, but every update must prove that it was based on the latest known version of the article.

Silent overwrites

Consider a typical editorial race condition:

  1. Editor A opens article 123.
  2. Editor B opens the same article.
  3. Editor A updates the headline and saves.
  4. Editor B, still looking at the older article state, updates the introduction and saves.

If the backend accepts Editor B's update without checking the article version, Editor A's headline change may be overwritten depending on how the payload is applied. Even when partial updates are used, stale state can still cause incorrect decisions around derived fields, validation context, publishing state, metadata, or structured content blocks.

How optimistic locking works

Optimistic locking assumes conflicts are possible but not constant. It does not lock an article when an editor opens it. Instead, the backend attaches a version token to the article representation. In Rails, this is conventionally lock_version.

Every successful update increments the version. Every update request must include the version that the client last received. The backend compares the submitted version with the current stored version:

Condition Backend behavior Result
Submitted version matches current article version Accept the update and increment the version The article is saved
Submitted version is older than current article version Reject the update as stale The frontend receives a conflict response

This turns an invisible data-loss problem into an explicit conflict. That is the important design property. The backend does not need to guess whether the stale update is safe; it rejects the stale write and gives the application a controlled path for resolution.

The /articles contract

The /articles endpoint should treat the article version as part of the article resource contract. Any response that returns an editable article should include the current version token. Any endpoint that mutates an article should require the last known version token from the frontend.

GET /articles/123
-> returns article data with lock_version: 7

Editor A updates article 123 with lock_version: 7
-> accepted
-> article lock_version becomes 8

Editor B updates article 123 with lock_version: 7
-> rejected
-> conflict: article is now at lock_version 8

The exact API shape can vary. The version can be sent in the JSON body, a request header, or an HTTP conditional request mechanism. The key requirement is semantic rather than syntactic: updates must be tied to the article version the editor actually reviewed.

Conflict response

A stale update should be represented as a conflict rather than as a generic validation failure. In HTTP APIs, 409 Conflict is the clearest status for this case because the request is syntactically valid but cannot be applied to the current state of the resource.

The response should provide enough information for the frontend to make a product decision. At minimum, it should indicate that the article has changed since it was loaded. In richer implementations, it can include the latest article version, changed fields, editor metadata, or a link to fetch the latest version.

After a conflict

Once a conflict is detected, the frontend has two realistic options.

Show an error and reload

This is the safest and simplest behavior. The editor is told that the article has been updated by someone else and that the current changes cannot be saved until the latest version is loaded.

This option is appropriate when editorial correctness is more important than convenience, when the article structure is complex, or when automatic merging would be risky. It keeps the system behavior obvious: stale edits are not silently applied.

Fetch latest and merge

The frontend can fetch the latest article, compare it with the editor's pending changes, and attempt to merge the two. This can work well for isolated field changes, such as one editor changing a title while another changes a summary. It becomes more difficult for structured body content, reordered blocks, embeds, metadata, taxonomy changes, or publishing state.

A merge flow should distinguish between automatic merges and human-reviewed merges. If both editors changed the same field or content block, the UI should surface the conflict rather than making an arbitrary decision.

Conflict strategy Best for Trade-off
Error and reload Safety, predictable editorial behavior, complex content More interruption for editors
Merge latest changes High-frequency editing, autosave, simple independent fields More frontend complexity and more edge cases

Why it fits a CMS

Article editing is usually a long-lived human workflow. An editor may keep a browser tab open for minutes or hours. Holding a database lock for that duration would be a poor fit. Optimistic locking avoids blocking other editors while still protecting the system from stale writes.

It also matches Rails conventions. Rails already provides optimistic locking when a locking column is present, and its behavior is centered around detecting stale writes rather than building a custom concurrency subsystem. That keeps the implementation aligned with the framework instead of adding a parallel mechanism that future maintainers must learn.

Pessimistic locking

Pessimistic locking takes the opposite assumption: conflicts are likely enough that the system should lock the record before changes proceed. In Rails, pessimistic locking uses database row locks, commonly expressed through SQL locking clauses such as FOR UPDATE.2

In an article editing workflow, pessimistic locking would look like this:

  1. The first editor starts editing article 123.
  2. The backend places a lock on that article row.
  3. Other editors attempting to update the same article must wait, fail, or be prevented from editing until the lock is released.

This can be useful for short, transaction-bound operations where correctness requires exclusive access. It is less suitable for a CMS editing screen because browser sessions are long-lived, editors can walk away, tabs can remain open, and the product experience becomes dependent on lock expiry and recovery behavior.

Approach Behavior CMS impact
Optimistic locking Allow parallel editing; reject stale saves Non-blocking and safer against silent overwrites
Pessimistic locking Lock the row before or during the update Can block editors and requires lock lifecycle management

Keeping complexity in the right place

John Ousterhout's A Philosophy of Software Design frames software design around managing complexity, especially cognitive load, change amplification, and unknown unknowns.5 Optimistic locking supports that goal when applied as a small, explicit resource contract: article representations carry a version, updates submit that version, and stale writes become conflicts.

This is also consistent with the idea of deep modules and information hiding. Rails hides the low-level stale-write detection behind Active Record's locking behavior. The API exposes only the necessary concept to the frontend: the article version. The frontend does not need to know how Rails implements the check internally, and the backend does not need to know how the editor UI will resolve a conflict.

The alternative, building a custom editing-lock system, would add more states, more recovery paths, more timing rules, and more operational edge cases. That may be justified for real-time collaborative editing, but it is a larger product and architecture decision. For standard CMS editing, optimistic locking is the smaller and clearer abstraction.

Implementation contract

  • Editable article responses include a version token, conventionally lock_version.
  • Article update requests submit the version token last seen by the frontend.
  • The backend rejects stale updates with a conflict response.
  • The frontend either displays a clear error or attempts a merge after fetching the latest article.
  • Pessimistic locking remains a comparison point, not the preferred default for long-lived editorial workflows.

Sources

  1. Rails API: ActiveRecord::Locking::Optimistic
  2. Rails API: ActiveRecord::Locking::Pessimistic
  3. AppSignal: Optimistic Locking in Rails REST APIs
  4. Engine Yard: A Guide to Optimistic Locking
  5. John Ousterhout, A Philosophy of Software Design