CAP does not apply here

Free preview 5 min to read

The CAP theorem is about distributed systems. This application isn't one, and understanding why takes you most of the way to understanding the theorem.

One node

From the senior course "Architecture: multi-region database": config/deploy.yml names one NOR_SERVER_IP, and both the Rails application and the PostgreSQL container run on it.

A single node can't be partitioned from itself. There's no network between the database and itself, so there are no messages to drop, and the condition the theorem is about doesn't exist.

That isn't a loophole. The theorem describes what happens when replicated state is separated by a network, and this application has no replicated state anywhere.

What it relies on instead

It relies on neither eventual consistency nor quorums. It relies on single-node linearizability, which PostgreSQL provides. It's the same property CAP calls consistency, and on one node it comes free.

# app/services/enrollment/award_badges_service.rb
# Unique index makes this safe under concurrency; only count fresh rows.
user_badge = ::Enrollment::UserBadge.create_or_find_by!(user: user, badge: badge)
badge if user_badge.previously_new_record?

That comment is a consistency argument, written down. Two concurrent requests both check whether a badge exists, both find nothing, and both insert. The unique index on [user_id, badge_id] means one insert succeeds and the other raises. create_or_find_by! handles the raise, and previously_new_record? tells the winner apart.

There's no lock and no coordination: the database's uniqueness guarantee does the work. Enrollment::UserBadge also declares validates :badge_id, uniqueness: { scope: :user_id }, but that Ruby validation isn't what makes this safe. Two concurrent requests both pass it, because each one queries before either inserts.

The Ruby validation gives a friendly error message, and the index gives the guarantee. That's the distinction, and the guarantee is what stops being free the moment there are two database nodes.

The uniqueness guarantees this application holds

On 2026-09-28 the application's own tables had twenty unique indexes. These are the ones the product leans on:

bookmarks                    [user_id, lesson_id]
enrollments                  [user_id, course_id]
lesson_completions           [user_id, lesson_id]
lesson_notes                 [user_id, lesson_id]
user_badges                  [user_id, badge_id]
experience_points            [user_id, idempotency_key]
identity_authentications     [provider, uid]
partner_course_sponsorships  [partner_id, course_id]
users                        [email], [username]
courses                      [slug]
lessons                      [slug]
levels                       [xp_required]

Almost all of these have a matching validates ... uniqueness on the model, and every one of those validations has a race. It queries, finds nothing and returns valid, while another request does the same.

The Ruby check is for the message, and the index is for the guarantee. The index is a property of one PostgreSQL node.

flowchart TB
  A["one PostgreSQL node"] --> B["no network between it and itself"]
  B --> C["no messages to drop"]
  C --> D["no partition. CAP has nothing to say"]
  A --> E["single-node linearizability"]
  E --> F["20 unique indexes carrying the real guarantee"]
  F --> G["the Ruby validations race; the index does not"]
  G --> H["exactly what stops being free with two nodes"]

When CAP starts applying

CAP applies the moment two nodes hold the same state with a network between them.

A read replica, step 4 of the previous course. Now there's a primary, a replica, a network, and a question about what a read returns when the network is down.

Two application servers with local caches. Solid Cache is a database table here, so one region's cache isn't the other's. A cached value that is correct in one region and stale in the other is replicated state with a network in between, whatever you call it.

A queue consumed from two places. The Solid Queue workers here all run on the one machine. Two regions consuming one queue need a distributed lock, which is a CAP problem in work clothes.

Add any of the three, and every question in this course becomes live. None of them is true today.

Why learn a theorem that does not apply

There are two reasons, and the second is the real one.

It tells you what you'd be giving up. The unique constraints above are free today, and they are the first casualty of a second node. Knowing that is how the multi-region course could say "the XP award path needs strong consistency" instead of discovering it later.

It's also the most misquoted result in the field. Most of what people believe about it is wrong in ways that lead to bad architecture: "we're an AP system", "pick two of three", "you have to choose". Lesson 3 covers those, and the correction matters more than the theorem.

The theorem fits on one page, and its misreadings have done more damage than the constraint it describes.

Key takeaways

  • One PostgreSQL node can't be partitioned from itself, so CAP describes a condition this application doesn't have.
  • The application relies on single-node linearizability: twenty unique indexes carry guarantees the matching Ruby validations can't.
  • create_or_find_by! with a unique index is a consistency argument, written down in a comment.
  • CAP starts applying with a read replica, per-region caches, or a queue consumed from two places.
  • The theorem fits on one page, and its misreadings have done more damage than the constraint it describes.
Lesson 1 of 7 · free

Keep going with lesson 2

The actual theorem

The definitions of consistency, availability and partition tolerance, the two-node proof, and why partition tolerance is not a choice.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...