Ruby on Rails
Effective database sharding requires structuring data boundaries around natural operational entities so that complex cross-shard joins and distributed transactions rarely occur.
AI-assisted summary of Sharding Is Hard: Let's Make It Easier! - Tess Griffin — original source. Check the original for full context.
Source published:
Key ideas
- Sharding becomes necessary when primary database write throughput saturates the largest available hardware, as read replicas can horizontally scale read traffic but do not relieve write bottlenecks. (0:10)
- Selecting an effective shard key depends on domain boundaries; partitioning by clinic rather than patient or practitioner isolates nearly all transactional writes and joins to a single machine while satisfying regulatory isolation. (4:21)
- Explicit shard mapping tables introduce a routing lookup dependency but enable zero-disruption tenant rebalancing and dedicated hardware assignment that mathematical hashing cannot provide. (8:27)
- Database proxies like PgDog can abstract the operational overhead of two-phase commits and scatter-gather fan-outs, but distributed transactions remain inherently slow and fragile compared to clean boundary modeling. (18:52)
Detailed notes
## Background Presented by Tess Griffin (thoughtbot) and published on YouTube by the Ruby on Rails channel on October 1, 2026, this presentation addresses how to scale out when a database outgrows vertical hardware limits. Based on a multi-clinic healthcare application, the talk explores selecting an effective shard key, routing queries via native Rails features or proxies like PgDog, and handling cross-shard joins and transactions under the guiding principle that effective sharding depends on clean boundaries.
## TL;DR When write throughput exhausts vertical scaling and read replicas, horizontal sharding distributes rows across discrete database nodes using a designated shard key. Evaluating a multi-clinic healthcare platform, Griffin demonstrates why sharding on a tenant boundary (`clinic_id`) confines writes, joins, and transactions locally, avoiding performance-degrading scatter-gather operations. Query routing, background jobs, and online resharding can be handled natively inside Rails or delegated to an external database proxy like PgDog. While scatter-gather reads and two-phase commit protocols exist for unavoidable multi-shard operations, their high latency and locking penalties mean true success lies in modeling data boundaries so cross-shard operations remain exceptionally rare.
## Atmosphere & style Griffin delivers a pragmatic, clear-eyed conference presentation that balances dry technical humor—such as examining a generic e-commerce app named 'Napify' or diagnosing databases actively 'on fire'—with rigorous architectural realism. The pacing is reassuring and educational, guiding viewers from the panic of ballooning $500,000 hosting bills through methodical trade-off evaluations of foreign keys and distributed protocols. It feels like an experienced senior engineer demystifying an intimidating rite of architectural passage, trading heroic distributed-systems complexity for grounded, disciplined domain modeling.
## Bulletin - [00:10](https://www.youtube.com/watch?v=yLoqg7UkryU&t=10s) Sharding becomes necessary when primary database write throughput saturates the largest available hardware, as read replicas can horizontally scale read traffic but do not relieve write bottlenecks. - [04:21](https://www.youtube.com/watch?v=yLoqg7UkryU&t=261s) Selecting an effective shard key depends on domain boundaries; partitioning by clinic rather than patient or practitioner isolates nearly all transactional writes and joins to a single machine while satisfying regulatory isolation. - [08:27](https://www.youtube.com/watch?v=yLoqg7UkryU&t=507s) Explicit shard mapping tables introduce a routing lookup dependency but enable zero-disruption tenant rebalancing and dedicated hardware assignment that mathematical hashing cannot provide. - [18:52](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1132s) Database proxies like PgDog can abstract the operational overhead of two-phase commits and scatter-gather fan-outs, but distributed transactions remain inherently slow and fragile compared to clean boundary modeling.
## Key takeaways - [00:10](https://www.youtube.com/watch?v=yLoqg7UkryU&t=10s) Vertical scaling hits hard physical and financial limits where larger instances either do not exist or become cost-prohibitive, forcing systems to partition data across multiple machines. - [00:10](https://www.youtube.com/watch?v=yLoqg7UkryU&t=10s) Read replicas fail to resolve write saturation because every replica maintains a complete copy of the data and must replicate every write processed by the primary instance. - [03:08](https://www.youtube.com/watch?v=yLoqg7UkryU&t=188s) Queries lacking the designated shard key trigger scatter-gather execution across all shards, multiplying database load and eliminating the performance advantages of sharding. - [04:21](https://www.youtube.com/watch?v=yLoqg7UkryU&t=261s) Evaluating candidate shard keys requires analyzing foreign key relationships; selecting an entity that does not bound related tables results in constant cross-shard joins. - [04:21](https://www.youtube.com/watch?v=yLoqg7UkryU&t=261s) Replicating static or global reference tables across every shard preserves local join capabilities without leaking foreign key lookups across network boundaries. - [08:27](https://www.youtube.com/watch?v=yLoqg7UkryU&t=507s) Computed modulo hashing makes fleet expansion difficult because changing the divisor forces a widespread rebalancing of tenants across every database instance. - [08:27](https://www.youtube.com/watch?v=yLoqg7UkryU&t=507s) Storing shard assignments in an explicit mapping table treats tenant placement as mutable data, allowing operators to isolate noisy tenants onto dedicated hardware via simple row updates. - [11:28](https://www.youtube.com/watch?v=yLoqg7UkryU&t=688s) Production data migration pipelines require careful rate throttling across historical backfills and change-data-capture catch-up replication to avoid crashing the live primary database. - [12:33](https://www.youtube.com/watch?v=yLoqg7UkryU&t=753s) Native Rails sharding supports multi-database routing via `connects_to`, but ad-hoc usage causes shard-routing logic to sprawl uncontrollably across controllers and services. - [14:49](https://www.youtube.com/watch?v=yLoqg7UkryU&t=889s) Background jobs should centralize shard switching within base class lifecycle hooks like `around_perform`, while queue backend tables must remain on a centralized, unsharded database. - [18:12](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1092s) Cross-shard relational joins cannot occur within a single database engine and require scatter-gather coordination to query instances in parallel and merge result sets in memory. - [18:52](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1132s) PostgreSQL supports two-phase commits via `PREPARE TRANSACTION`, but coordinator crashes before the commit phase leave orphaned row locks holding database resources indefinitely. - [18:52](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1132s) While database proxies can manage distributed two-phase commit recovery protocols, they cannot eliminate the latency and blocking costs inherent to multi-node network transactions. - [21:44](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1304s) Successful sharding is fundamentally a data modeling problem: establishing clean tenant boundaries eliminates the need to rely on complex distributed transactions.
## Detailed walkthrough ### [00:10](https://www.youtube.com/watch?v=yLoqg7UkryU&t=10s) The Limits of Vertical Scaling and When to Shard
Tess Griffin, a senior developer at thoughtbot, opens the talk by diagnosing a classic infrastructure nightmare: an application that has lived comfortably on a single database for years, growing steadily until routine vertical scaling hits a wall. Teams instinctively keep upgrading the hardware—making the box "bigger and bigger and bigger"—until the hosting bill tops $500,000 a month and the database still buckles under the load. In this state, once-instantaneous queries crawl, the slightest traffic spike threatens to set the entire system on fire, and further vertical scaling is off the table simply because larger instances either do not exist or cost an impossible fortune.
The first step in triage is measurement. Teams need to interrogate Postgres directly to understand whether the workload is read-heavy or write-heavy, identify which specific tables absorb the bulk of the write volume, and inspect which tables have grown massive. If read traffic is the culprit, there is an easier, cheaper remedy: read replicas. A replica provides a live copy of the dataset to handle read queries, allowing reads to scale horizontally simply by provisioning more servers. However, standard SQL architectures deliberately trade slower writes for faster reads; every read replica still maintains a full copy of every row, and the primary database alone must process every write. When massive tables bog down every associated read, write, and index, and write throughput has saturated the largest available box, vertical scaling is exhausted.
Horizontal scaling through sharding is the solution. Rather than relying on a single monolithic machine, sharding breaks a single database into multiple distinct databases, spreading rows across them so that every individual machine handles only a slice of total writes and stores only a fraction of the total dataset.
- **Vertical Scaling Limitations**: Upgrading hardware eventually reaches physical or financial limits where "the next size up doesn't exist." - **Read Replicas**: Distribute read load effectively, but do nothing to alleviate primary write bottlenecks since every write still routes to one primary. - **The Core Mechanic of Sharding**: Splitting rows across distinct machines using a dedicated column value—the **shard key**.
---
[03:08](https://www.youtube.com/watch?v=yLoqg7UkryU&t=188s) The Shard Key and the Ideal Case: "Napify"
To shard successfully, an application must determine how rows are distributed among instances by designating a specific column as the shard key. The value of this column dictates where a row lives. When incoming queries include the shard key, the database layer routes them directly to the single relevant shard. Conversely, if a query lacks the shard key, it must scatter across all shards simultaneously: a single query becomes three or more, every machine halts to evaluate it, and most return nothing. Scatter-gather queries quickly destroy any performance gains sharding was supposed to provide.
```mermaid flowchart TD accTitle: Shard Key Query Routing accDescr: Routing behavior comparing a query with a shard key targeting one shard versus a keyless query broadcasting to all shards.
subgraph DirectRouting["Targeted Query (With Shard Key)"] Q1["Query with Shard Key"] --> S1["Target Shard"] end
subgraph ScatterRouting["Scatter-Gather (Without Shard Key)"] Q2["Query without Shard Key"] Q2 --> SA["Shard A (No Match)"] Q2 --> SB["Shard B (Row Found)"] Q2 --> SC["Shard C (No Match)"] end ```
Griffin introduces a clean baseline scenario dubbed "Napify" (noting with a grin that "any resemblance to other storefront platforms is coincidental"). An e-commerce platform represents an ideal sharding problem because the domain boundaries are naturally hierarchical: almost every database table hangs cleanly off of a shop. Drawing a conceptual boundary around a single `shop_id` captures virtually all related transactional records without any messy dependencies spilling outside the line. In that architecture, `shop_id` serves as a clean shard key, while cross-cutting concerns like admin users, billing tiers, and feature flags comfortably remain behind on a shared global primary database.
- **Targeted Query**: Contains the shard key; executes against exactly one machine. - **Scatter-Gather Query**: Lacks the shard key; executes across every shard in parallel, multiplying load and nullifying the benefits of sharding. - **Clean Tenancy**: In platforms like Napify, drawing a boundary around a single shop isolates almost all operational tables with zero external leaks.
---
[04:21](https://www.youtube.com/watch?v=yLoqg7UkryU&t=261s) Evaluating Shard Keys for a Healthcare App
The talk shifts to a messy real-world problem: an application called "TheirChart," an original platform serving hundreds of clinics from a single Postgres database that is currently "on fire." The application never planned for sharding, and its two biggest operational bottlenecks are the `visits` and `audit_logs` tables. Because the platform must maintain HIPAA compliance, every access of sensitive data generates an audit entry, causing `audit_logs` to swell uncontrollably. To resolve the bottleneck, the chosen shard key must split both `visits` and `audit_logs`. Because a table can only be partitioned by a column it actually contains, Griffin evaluates the four ID columns present on a `visit`: `patient_id`, `practitioner_id`, `visit_id`, and `clinic_id`.
```mermaid flowchart LR accTitle: Visit Table Foreign Key Candidates accDescr: The four candidate keys evaluated for sharding the visits and audit logs tables.
V["Visit Record"] V --> P["patient_id"] V --> PR["practitioner_id"] V --> VI["visit_id"] V --> C["clinic_id (Chosen Key)"] ```
The speaker tests each potential candidate by making a conceptual cut through the schema to see what breaks: 1. **Patient (`patient_id`)**: While intuitively tempting because the application revolves around patients, patients often visit multiple clinics. Cutting across patients leaves half of the application's queries unable to route effectively, while other operations do not carry a patient context at all, triggering constant scatter-gather queries. 2. **Practitioner (`practitioner_id`)**: Practitioners often work across multiple clinics, and very few ancillary tables relate directly to practitioner records. A key that connects to almost nothing leaves most queries stranded with no shard routing information. 3. **Visit ID (`visit_id`)**: Sharding directly on the primary key of the burning table would evenly distribute visit writes. However, a visit owns almost nothing and touches everything; almost all contextual details live in other tables, forcing every subsequent query to perform expensive cross-shard joins. 4. **Clinic (`clinic_id`)**: Sharding on the clinic isolates writes cleanly within bounded boundaries. Almost every write generated by a clinic stays inside its own silo, meaning joins and transactions remain simple, single-database operations on one machine.
To handle the few cross-cutting edges with clinic sharding, remedies—which are universal across all clinics—are replicated across every shard so queries can join against them locally. Meanwhile, global administrative functions like billing remain on the primary database. Furthermore, this boundary aligns neatly with real-world healthcare compliance: because each clinic operates as an independent covered entity, keeping patient records isolated per clinic satisfies legal privacy boundaries while providing blast-radius containment if a shard fails.
- **Blast Radius Protection**: If a shard suffers downtime or security compromise, the fallout is restricted entirely to that shard's clinics rather than crashing the whole platform. - **Replicated Tables**: Static reference tables like `remedies` are copied to all shards to ensure joins remain local. - **Primary Data**: Global data disconnected from clinic workflows (such as billing) stays centralized on the primary.
---
[08:27](https://www.youtube.com/watch?v=yLoqg7UkryU&t=507s) Assigning Shards: Computed Hashes vs. Stored Shard Maps
Once `clinic_id` is chosen, the architecture faces two distinct implementation requirements: migrating the data onto individual shards and routing subsequent queries accurately. These tasks can be handled within Rails itself or delegated to an external SQL proxy such as PgDog for Postgres, or Vitess / PlanetScale for MySQL. Before any data can migrate, however, every clinic must be assigned a home shard. Systems accomplish this either by computing the mapping on the fly or storing it in a routing table.
```mermaid flowchart TD accTitle: Shard Assignment Approaches accDescr: Comparison between computed hash routing and stored shard map routing.
subgraph Computed["Approach 1: Computed (Hashing)"] CID1["clinic_id"] --> HASH["clinic_id % n"] HASH --> SHARD1["Shard N"] end
subgraph Stored["Approach 2: Stored Shard Map"] CID2["clinic_id"] --> MAP[("shard_map Table")] MAP --> SHARD2["Assigned Shard (Explicit)"] end ```
The computed approach runs the `clinic_id` through a hashing function, such as a naive modulo operation (`clinic_id % n`, where `n` is the total shard count). Griffin cautions that simple modulo hashing is ill-advised in production because altering `n` reshuffles virtually every clinic across different shards. While more sophisticated consistent hashing algorithms exist—Griffin points attendees toward Martin Kleppmann's book, *Designing Data-Intensive Applications*, for an in-depth treatment—any computed approach leaves placement entirely to the mathematical function. Teams cannot manually rebalance or isolate an individual tenant.
The alternative approach stores the shard mapping in an explicit database table with one row per clinic. When a `clinic_id` is provided, the table returns its designated shard. Storing the mapping treats placement as mutable data, granting operators full control: - **Tenant Isolation**: A massive clinic overwhelming its neighbors can be moved to a dedicated hardware shard with a single row update. - **Architectural Trade-Off**: Every database lookup must query this mapping table first. If the mapping table goes down, access to all underlying shards fails. - **Practical Mitigation**: The shard map table is tiny, reads are easily cached, and writes occur infrequently (only when clinics are onboarded or migrated).
---
[11:28](https://www.youtube.com/watch?v=yLoqg7UkryU&t=688s) Migrating Data: DIY Pipelines vs. Proxy-Based Resharding
Moving live production data onto newly partitioned shards presents substantial operational risk. Doing the migration in-house requires building custom tooling: developers must backfill existing records, run catch-up replication to capture writes that landed during the backfill process, and orchestrate an atomic cutover. The copy operations must be throttled carefully; otherwise, the migration itself can exhaust database capacity and bring down production.
Alternatively, teams can offload this operational burden to a database proxy like PgDog. With PgDog, engineers configure a Postgres publication and initiate a reshard command. The application continues running and serving traffic while PgDog handles backfilling, change data capture catch-up, and cutover behind the scenes. The foundational migration steps remain identical, but using a proxy transfers the execution risk and automation to battle-tested infrastructure.
- **DIY Migration Steps**: Backfill historical data, replicate live catch-up writes, and coordinate final cutover—all with rate throttling to prevent database failure. - **Proxy Automation**: Running PgDog's resharding provisions empty shard databases and populates them using Postgres publications while the application operates uninterrupted.
---
[12:33](https://www.youtube.com/watch?v=yLoqg7UkryU&t=753s) Routing Queries: Rails Native Sharding and the Sprawl Problem
When an incoming request hits the stack, the system must decide which shard to query. This routing decision can either live at the proxy layer—where the application connects to what appears to be a single standard database while the proxy routes the SQL statements—or directly inside Rails. Rails has supported multiple databases since version 6.0 and introduced native sharding support in Rails 6.1, functioning identically on Postgres and MySQL.
Configuring Rails begins in `config/database.yml`, where each shard is defined with its own distinct connection pool and dedicated read replica. In the domain layer, an abstract application record binds the shards together using Rails' `connects_to` directive, configuring both a `writing` role and a `reading` role for every shard. Tenant-scoped models inherit from this base class, while universally shared reference tables (`remedies`) and global services (`billing`) retain their respective configurations.
To execute queries against a tenant, Rails relies on a clean connection-switching primitive:
```ruby shard_name = ShardMap.shard_for(clinic_id) ActiveRecord::Base.connected_to(shard: shard_name, role::writing) do # All queries inside this block execute against the clinic's shard @clinic.visits.all end ```
Rails answers only one specific question: *Which shard do I talk to?* However, as Griffin highlights, this introduces a subtle architectural challenge: code sprawl. While the shard key is technically just a single column value, the moment an application shards, every query requires that context. Developers find themselves wrapping database access in `connected_to` blocks inside the `VisitsController`, repeating the pattern across background workers like `ReminderJob`, and watching shard routing logic spread throughout the codebase.
- **Rails Version Milestones**: Multiple database support arrived in Rails 6.0; first-class sharding arrived in Rails 6.1. - **Connection Configuration**: Configured via `database.yml` and wired using `connects_to(shards: {... })` on an abstract model. - **Context Creep**: Sharding requires the shard key everywhere; without strict abstraction boundaries, `connected_to` blocks quickly proliferate across controllers, service objects, and background jobs.
[14:49](https://www.youtube.com/watch?v=yLoqg7UkryU&t=889s) Routing Queries and Background Jobs
When preparing an application for sharding, the biggest architectural hazard is letting routing logic bleed across the entire codebase. If every controller, model, or service has to independently figure out which shard a clinic belongs to, developers are guaranteed to miss places. The cardinal rule of sharding is containment: keep routing in exactly one place. Out of the box, Ruby on Rails provides built-in configuration for multi-database and sharded environments, establishing a single source of truth that knows how to map a request to a shard.
For teams wanting to take separation of concerns a step further, query routing can be moved entirely outside the application layer and handed over to an intelligent database proxy such as PgDog. With a proxy, the Rails application simply passes the shard key, and the proxy takes care of connection routing under the hood. There is no in-app shard map to maintain and no resolver to write. As far as the Rails code is concerned, it goes back to believing it is speaking to a single, standard database while still reaping the horizontal scaling benefits of sharding.
```mermaid flowchart TD accTitle: Shard Routing in Rails Application vs Database Proxy accDescr: Diagram contrasting in-app shard routing via Rails connected_to with proxy-based routing via PgDog.
subgraph AppApproach["Application-Level Routing (Rails)"] direction TB A["Incoming Request / Job"] --> B["Rails Shard Resolver / Map"] B -->|"ActiveRecord::Base.connected_to"| C[("Target Shard")] end
subgraph ProxyApproach["Proxy-Level Routing (e.g., PgDog)"] direction TB D["Incoming Request / Job"] --> E["Set Shard Key (e.g. SET LOCAL)"] E --> F["Database Proxy"] F --> G[("Target Shard")] end ```
The exact same discipline must be applied to background processing. Rather than scattering shard-awareness across dozens of workers, background jobs should be contained through `ApplicationJob`: - **Job Arguments:** Pass the routing key (`clinic_id`) explicitly into the job arguments. - **Single Hook in ApplicationJob:** Implement an `around_perform` callback in `ApplicationJob` that extracts the `clinic_id` and wraps execution in ActiveRecord’s `connected_to(shard:...)`. - **Fail Fast:** If an engineer enqueues a job without a clinic identifier, the base class immediately raises an error instead of letting the job blindly execute against the wrong shard or fallback database. - **Proxy Pattern with SQL:** When using a database proxy, the worker connects to the standard proxy endpoint and sets the sharding key within a transaction using `SET LOCAL sharding_key =...` in the `around_perform` block. - **Queue Database Placement:** Queue backends like Solid Queue are database-backed and must never be sharded across tenant shards; their tables should remain on the primary database or inside an independent, dedicated database (the same isolation principles apply when utilizing Redis-backed queues like Sidekiq).
[17:16](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1036s) Resharding and Handling Growth
A sharding architecture must anticipate the day when the original set of shards runs out of capacity. How an application handles expansion depends heavily on how tenant allocation was designed from the beginning. If the system relies on a mathematical hashing function to determine shard placement, changing the modulus or hash algorithm forces widespread data migration across the entire fleet.
By contrast, maintaining an explicit shard mapping table requires more work up front, but it "pays for itself the first time you need to add more shards." In an explicit lookup model, when an individual tenant becomes massive, an engineer can spin up a dedicated database shard, point that specific clinic’s ID to the new shard in the map table, and migrate only that clinic’s records without disturbing neighboring tenants. When using a database proxy, resharding can often occur dynamically while the application continues serving live traffic, avoiding application-level mapping bugs because the Rails app never managed an explicit mapping table to begin with.
[18:12](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1092s) Cross-Shard Joins and Scatter-Gather
Because choosing a good shard key keeps all of a single clinic's related records co-located on the same physical database instance, the overwhelming majority of relational SQL joins execute locally and never cross shard boundaries. However, certain queries—such as cross-organization reporting, administrative analytics, or global aggregations—unavoidably span multiple databases.
Because a database engine cannot perform a relational join across separate physical database instances in a single round trip, applications must resort to the "scatter-gather" pattern: - **Scatter:** The application loops through every shard in the fleet and dispatches the query to each database in parallel or sequence. - **Gather:** The application collects the independent result sets into memory and aggregates, sums, or merges the answers within Ruby. - **Proxy Fan-Out:** An intelligent proxy can handle scatter-gather transparently. The developer writes a single standard SQL query; the proxy fans the read out to every shard across the cluster, merges the rows or aggregate totals, and returns a single unified result set to Rails.
[18:52](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1132s) Cross-Shard Transactions and Two-Phase Commit
While distributed reads can be aggregated with scatter-gather, cross-shard writes present a much harsher architectural challenge. On a single database node, transactions are strictly atomic: a write either completely succeeds or completely fails as an indivisible unit. Once data is split across machines that communicate over a network, true atomicity in the traditional sense disappears due to the fundamental constraints of distributed systems. Engineers face an unavoidable choice: either block and wait for every single machine to agree before returning, or return an immediate answer and let records catch up asynchronously later.
```mermaid flowchart TD accTitle: Two-Phase Commit Protocol Flow accDescr: Step-by-step visualization of the Prepare and Commit phases in a two-phase commit protocol across multiple database shards.
subgraph Phase1["Phase 1: Prepare"] C1["Coordinator"] -->|"PREPARE TRANSACTION"| S1["Shard 1 (Locks Row)"] C1 -->|"PREPARE TRANSACTION"| S2["Shard 2 (Locks Row)"] S1 -->|"Promise: OK"| C1 S2 -->|"Promise: OK"| C1 end
subgraph Phase2["Phase 2: Commit"] C1 -->|"COMMIT PREPARED"| S1 C1 -->|"COMMIT PREPARED"| S2 end ```
When multi-shard consistency is strictly required, the system must utilize a two-phase commit (2PC) protocol: - **Phase 1 (Prepare):** The coordinator asks every participating shard whether it can execute the operation. Each shard locks the required rows, reserves state, and returns an explicit promise that it can commit. - **Phase 2 (Commit):** Only if every single shard responds affirmatively does the coordinator issue the final commit instruction. If even one shard fails or times out, the coordinator instructs all shards to roll back. - **The Cost of 2PC:** Two-phase commit is an inherently blocking protocol where the entire transaction moves at the speed of the slowest participating shard. - **Coordinator Failure and Orphaned Locks:** PostgreSQL provides built-in support for `PREPARE TRANSACTION`, which allows prepared state to survive database crashes. However, PostgreSQL will not automatically clean up prepared transactions if the coordinating application crashes midway through the handshake. If the coordinator dies before issuing the commit, participating shards are left stranded holding active row locks indefinitely. - **Hand-Rolling vs. Proxies:** Hand-rolling a 2PC coordinator requires writing extensive recovery plumbing, health checkers, and cleanup daemons to detect and resolve stranded locks. A database proxy can execute the two-phase commit protocol automatically behind a standard transaction interface, saving developers from building distributed transaction machinery from scratch.
Even with a proxy handling the protocol, the underlying physical costs remain: locks are held across network round trips, and latency is high. The proxy enforces an all-or-nothing outcome across shards, but "what it can't give you is atomicity. That left when data split." The only sustainable strategy is to choose a sharding key that ensures cross-shard writes account for only an infinitesimal fraction of overall application traffic.
[21:44](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1304s) The Core Lesson: Good Sharding Is Good Boundaries
A database proxy can hide the operational friction of distributed architecture, but it cannot bend the laws of physics. Cross-shard joins remain fundamentally slower than local joins, and two-phase commits remain inherently fragile and resource-heavy. Winning at sharding does not mean heroically engineering your way through cross-shard complexities; success comes from structuring data boundaries so that complex cross-shard edge cases rarely happen in the first place.
When using native Rails sharding, the application team directly owns and maintains every single moving part: schema migrations, shard lookup mappings, routing resolvers, scatter-gather aggregations, and distributed transactions. Introducing a database proxy relieves the application of that plumbing burden, allowing Rails to treat the database cluster as a single logical entity while running resharding workflows online.
Ultimately, sharding is won or lost at the data modeling phase: - Do not attempt to defeat sharding by "working harder on the hard parts." - Arrange models and foreign keys so that each tenant's entire operational footprint lives self-contained in one physical location. - Shard on the natural boundary that is easiest to cleanly separate. - "Good sharding is good boundaries."
## Notable quotes > [00:10](https://www.youtube.com/watch?v=yLoqg7UkryU&t=10s) bigger and bigger and bigger — Tess Griffin
> [00:10](https://www.youtube.com/watch?v=yLoqg7UkryU&t=10s) the next size up doesn't exist. — Tess Griffin
> [03:08](https://www.youtube.com/watch?v=yLoqg7UkryU&t=188s) any resemblance to other storefront platforms is coincidental — Tess Griffin
> [04:21](https://www.youtube.com/watch?v=yLoqg7UkryU&t=261s) on fire — Tess Griffin
> [12:33](https://www.youtube.com/watch?v=yLoqg7UkryU&t=753s) Which shard do I talk to? — Tess Griffin
> [17:16](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1036s) pays for itself the first time you need to add more shards. — Tess Griffin
> [18:52](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1132s) what it can't give you is atomicity. That left when data split. — Tess Griffin
> [21:44](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1304s) working harder on the hard parts. — Tess Griffin
> [21:44](https://www.youtube.com/watch?v=yLoqg7UkryU&t=1304s) Good sharding is good boundaries. — Tess Griffin
## Mentioned Tess Griffin, thoughtbot, Ruby on Rails, Active Record, PostgreSQL, MySQL, PgDog, Vitess, PlanetScale, Martin Kleppmann, Designing Data-Intensive Applications, Solid Queue, Sidekiq, Shopify, HIPAA
## Video description When your database has outgrown the biggest box you can buy, sharding is the next step. Using a multi-clinic healthcare app as the example, Tess Griffin (thoughtbot) covers how to choose a shard key and how to route queries using Rails' built-in sharding or a proxy like PgDog. It also covers what to do about cross-shard joins and transactions. The core lesson is that good sharding comes down to good boundaries.
*** Thank you Shopify for sponsoring the editing and post-production of these videos. Check out insights from the Engineering team at: https://shopify.engineering/
#RailsWorld #RubyOnRails #Postgres #Sharding #Databases #Scaling
[Source](https://www.youtube.com/watch?v=yLoqg7UkryU)