<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harry Phung</title>
    <description>The latest articles on DEV Community by Harry Phung (@quangpv).</description>
    <link>https://dev.to/quangpv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4176839%2F8e9cb2d8-e949-4c40-9310-4142682a6490.jpg</url>
      <title>DEV Community: Harry Phung</title>
      <link>https://dev.to/quangpv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quangpv"/>
    <language>en</language>
    <item>
      <title>System design: Bloom Filters &amp; Use Cases</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Sun, 11 Oct 2026 10:03:27 +0000</pubDate>
      <link>https://dev.to/quangpv/system-design-bloom-filters-use-cases-4mgg</link>
      <guid>https://dev.to/quangpv/system-design-bloom-filters-use-cases-4mgg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxn95w165n63x2aykqp7l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxn95w165n63x2aykqp7l.png" alt="System design: Bloom Filters &amp;amp; Use Cases" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When designing a system with millions of users or resources, we often use Redis caching to reduce database load. But what happens when requests keep looking for IDs that do not exist? Redis keeps returning cache misses, the database still checks every ID, and legitimate users can experience slower responses as query volume grows.&lt;/p&gt;

&lt;p&gt;This is also a useful scenario to discuss in a system design interview: &lt;strong&gt;how do you avoid unnecessary queries when bots or attackers send large numbers of requests for nonexistent resource IDs?&lt;/strong&gt; This article explores how Bloom filters in Redis screen these requests, how the workflow changes, and what the benefits and limitations look like through concrete examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Overview: Reject Nonexistent IDs Before Reading Data
&lt;/h2&gt;

&lt;p&gt;A Bloom filter provides a lightweight screening step before data retrieval. When the filter is complete and synchronized, a negative result lets the application return “not found” immediately. A positive result continues to the cache and, when necessary, the database.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Without a Bloom filter:&lt;/strong&gt; nonexistent ID → Redis cache miss → database lookup → 404.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With a Bloom filter:&lt;/strong&gt; nonexistent ID → usually a negative filter result → 404, skipping the cached payload and database lookup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benefit:&lt;/strong&gt; fewer unnecessary queries, especially when bots or attackers keep requesting different nonexistent IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A simple example:&lt;/strong&gt; out of one million requests, suppose 500,000 look for nonexistent data. If the filter rejects all of them, the system avoids &lt;strong&gt;500,000 cached-payload reads and 500,000 database queries&lt;/strong&gt;. With a 1% false-positive rate on this group, the expected number avoided is &lt;strong&gt;495,000 at each layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A Bloom filter hosted in Redis still requires a Redis membership check. These figures describe cached-payload reads and database queries skipped, rather than API requests disappearing or all Redis accesses being eliminated.&lt;/p&gt;

&lt;p&gt;A Bloom filter returns either &lt;strong&gt;negative&lt;/strong&gt; (definitely absent from the set loaded into the filter) or &lt;strong&gt;positive&lt;/strong&gt; (possibly present, requiring further verification). A positive result can be a false positive. Use negative results to skip lookups only when the filter is complete and synchronized. If a new user exists in the database but has not been added to the filter, the application could incorrectly return 404.&lt;/p&gt;

&lt;p&gt;For a closer look at the internal mechanism, read &lt;a href="https://systemdesign.one/bloom-filters-explained?ref=blog.quangpv.top" rel="noopener noreferrer"&gt;Bloom Filters Explained — System Design&lt;/a&gt;, which illustrates insertion, membership checks, and false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Requests for Missing Data Still Consume Resources
&lt;/h2&gt;

&lt;p&gt;Suppose you operate a service with 10 million users. A typical user lookup follows this path:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User lookup request → Redis cache → cache miss → database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This works well for existing users. But if a client keeps sending IDs that do not exist, Redis keeps missing and the database still checks every request.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;cache penetration&lt;/strong&gt; : requests for nonexistent data pass through the cache and reach the database. It differs from a cache stampede, where many requests simultaneously fetch the same existing data after its cache entry expires.&lt;/p&gt;

&lt;p&gt;An indexed SELECT may be fast. Tens of thousands of unnecessary SELECTs per second still consume connections, CPU, network bandwidth, and operating budget.&lt;/p&gt;

&lt;p&gt;The question we want to answer before those lookups is: &lt;strong&gt;could this ID exist in the dataset?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How Attackers Exploit Cache Misses to Overload the Database
&lt;/h2&gt;

&lt;p&gt;An attacker does not need access to the data to create load. They can repeatedly send requests containing nonexistent resource IDs that still match the format accepted by the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without a Bloom filter:&lt;/strong&gt; request a nonexistent ID → Redis cache miss → database query → no record → 404. Switch to another nonexistent ID, and the entire workflow repeats.&lt;/p&gt;

&lt;p&gt;If the application does not cache “not found” results, each request can trigger a database query. Even with negative caching, constantly changing the ID makes previously cached results difficult to reuse.&lt;/p&gt;

&lt;p&gt;The result can be a busy connection pool, higher database CPU usage, and longer waits for legitimate requests. The cache still serves valid data, but it does not stop requests for nonexistent IDs from reaching the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a Bloom filter:&lt;/strong&gt; request a nonexistent ID → check the filter in Redis → most checks return negative → return 404 immediately, without reading the cached payload or querying the database. Only positive results proceed.&lt;/p&gt;

&lt;p&gt;This reduces database load caused by cache penetration. Application and Redis traffic remain, so rate limiting is still necessary. The configured false-positive rate also does not guarantee the same reduction for every attack pattern: repeatedly requesting an ID that produces a false positive can still bypass the filter. Negative caching and rate limits help address this case.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A Redis Cache Miss Does Not Mean the User Does Not Exist
&lt;/h2&gt;

&lt;p&gt;Redis typically caches only part of the dataset. Keys can expire or be evicted. The database remains the source of truth.&lt;/p&gt;

&lt;p&gt;The Bloom filter in this article therefore represents &lt;strong&gt;user IDs in the database&lt;/strong&gt; , rather than just keys currently present in the cache.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application receives a request to look up a user by ID.&lt;/li&gt;
&lt;li&gt;If the filter is unavailable or cannot be trusted, use the normal cache → database workflow.&lt;/li&gt;
&lt;li&gt;If the filter is ready, check the ID using the Bloom filter in Redis.&lt;/li&gt;
&lt;li&gt;Negative: return “not found,” skipping the profile cache and database.&lt;/li&gt;
&lt;li&gt;Positive: read the profile cache; on a hit, return the user.&lt;/li&gt;
&lt;li&gt;Cache miss: query the database. If the user exists, update the cache and return the data. Otherwise, return “not found.”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four scenarios illustrate the difference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Bloom filter&lt;/th&gt;
&lt;th&gt;Profile cache&lt;/th&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User exists and is cached&lt;/td&gt;
&lt;td&gt;Positive&lt;/td&gt;
&lt;td&gt;Hit&lt;/td&gt;
&lt;td&gt;Not queried&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User exists, but cache entry expired&lt;/td&gt;
&lt;td&gt;Positive&lt;/td&gt;
&lt;td&gt;Miss&lt;/td&gt;
&lt;td&gt;User found&lt;/td&gt;
&lt;td&gt;200, refresh cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User does not exist&lt;/td&gt;
&lt;td&gt;Usually negative&lt;/td&gt;
&lt;td&gt;Not read&lt;/td&gt;
&lt;td&gt;Not queried&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User does not exist, but filter returns a false positive&lt;/td&gt;
&lt;td&gt;Positive&lt;/td&gt;
&lt;td&gt;Miss&lt;/td&gt;
&lt;td&gt;User not found&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A false positive causes an extra lookup, but must never cause the system to return a user that does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Load Examples: How Many Database Queries Can You Avoid?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1. One Million Requests, Half for Nonexistent Data
&lt;/h3&gt;

&lt;p&gt;Suppose the system receives &lt;strong&gt;1,000,000 resource lookup requests&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;500,000 requests look for existing resources.&lt;/li&gt;
&lt;li&gt;500,000 requests look for nonexistent resources, using different IDs.&lt;/li&gt;
&lt;li&gt;The filter is complete and synchronized; there is no negative cache or other blocking layer.&lt;/li&gt;
&lt;li&gt;The normal workflow reads Redis cache first and queries the database on a miss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the 500,000 requests targeting nonexistent resources:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation for nonexistent IDs&lt;/th&gt;
&lt;th&gt;Without a Bloom filter&lt;/th&gt;
&lt;th&gt;With a filter, all results negative&lt;/th&gt;
&lt;th&gt;With a filter, 1% false positives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bloom filter checks in Redis&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached-payload reads in Redis&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;About 5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database queries after cache misses&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;About 5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reads avoided at each cache/database layer&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;About 495,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Bloom filter &lt;strong&gt;keeps nearly 500,000 requests out of the data retrieval workflow&lt;/strong&gt;. With a 1% false-positive rate, it is expected to reject 495,000 requests: &lt;strong&gt;49.5% of all incoming requests&lt;/strong&gt; and &lt;strong&gt;99% of requests for nonexistent data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The 500,000 requests for existing resources still continue through data retrieval. Their database query count depends on the cache hit rate. For example, with a 90% cache hit rate, existing resources contribute another 50,000 database queries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Total database queries across one million requests&lt;/th&gt;
&lt;th&gt;Query count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Without a Bloom filter&lt;/td&gt;
&lt;td&gt;500,000 + 50,000 = &lt;strong&gt;550,000&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With a Bloom filter, 1% false positives&lt;/td&gt;
&lt;td&gt;5,000 + 50,000 = &lt;strong&gt;55,000&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries avoided&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;495,000&lt;/strong&gt; , a &lt;strong&gt;90% reduction&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are calculations under stated assumptions, rather than benchmark results. Since the filter lives in Redis, all one million requests still check it, and positive results also read the cached payload. The benefit is a substantial reduction in database queries and skipping cached-payload reads for negative results; it does not guarantee fewer total Redis commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2. An Example in Requests per Second
&lt;/h3&gt;

&lt;p&gt;Suppose the API receives 50,000 requests per second:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;80% target nonexistent users: 40,000 requests per second.&lt;/li&gt;
&lt;li&gt;20% target existing users: 10,000 requests per second.&lt;/li&gt;
&lt;li&gt;Existing users have a 90% cache hit rate.&lt;/li&gt;
&lt;li&gt;The filter contains all user IDs and has a 1% false-positive rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Without a Bloom filter:&lt;/strong&gt; 40,000 nonexistent-user requests plus 1,000 cache misses for existing users produce &lt;strong&gt;41,000 database queries per second&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a Bloom filter:&lt;/strong&gt; 40,000 × 1% plus 1,000 produces an expected &lt;strong&gt;1,400 database queries per second&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Under these assumptions, database queries drop by approximately &lt;strong&gt;96.6%&lt;/strong&gt;. This is an illustrative calculation. Repeated requests, ID distribution, cache hit rate, and filter synchronization all affect actual results.&lt;/p&gt;

&lt;p&gt;With Redis's Bloom filter support, each request still accesses Redis for a membership check. The application must load the IDs and keep them synchronized; Redis does not automatically read the database to maintain this set.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Illustrated Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1. Profile APIs with Millions of Users
&lt;/h3&gt;

&lt;p&gt;A community application has 10 million users. Alongside legitimate profile views, the API receives invalid links and ID-probing requests from bots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without a Bloom filter:&lt;/strong&gt; every nonexistent ID causes a Redis cache miss followed by a database lookup that finds no user. With 10,000 requests per second targeting different nonexistent IDs, the database could receive 10,000 queries per second just to confirm their absence, assuming no other blocking layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a Bloom filter:&lt;/strong&gt; request → check the user ID set in Redis → return 404 on a negative result; otherwise read the cache and database as needed. If approximately 1% of these nonexistent-ID requests produce false positives, their expected database query count falls to about 100 per second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; most unnecessary queries are rejected before reaching the database, reducing competition for resources with legitimate users. This is an illustrative calculation, rather than a guaranteed reduction for every workload.&lt;/p&gt;

&lt;p&gt;For existing users, the effect differs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Example request&lt;/th&gt;
&lt;th&gt;Without a filter&lt;/th&gt;
&lt;th&gt;With a filter&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alex, ID 1001, profile cached&lt;/td&gt;
&lt;td&gt;Cache hit → return profile&lt;/td&gt;
&lt;td&gt;Filter positive → cache hit → return profile&lt;/td&gt;
&lt;td&gt;Neither queries the database; the filter adds a check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ben, ID 1002, cache entry expired&lt;/td&gt;
&lt;td&gt;Cache miss → database → refresh cache&lt;/td&gt;
&lt;td&gt;Filter positive → cache miss → database → refresh cache&lt;/td&gt;
&lt;td&gt;A database read is still required for the existing user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ID 99999999 does not exist, filter negative&lt;/td&gt;
&lt;td&gt;Cache miss → no database record → 404&lt;/td&gt;
&lt;td&gt;Filter negative → 404&lt;/td&gt;
&lt;td&gt;Avoids the database query and cached-payload read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonexistent ID producing a false positive&lt;/td&gt;
&lt;td&gt;Cache miss → no database record → 404&lt;/td&gt;
&lt;td&gt;Filter positive → cache miss → no database record → 404&lt;/td&gt;
&lt;td&gt;No database query reduction for this request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  6.2. Resource Lookups: Posts, Products, and Files
&lt;/h3&gt;

&lt;p&gt;A store has millions of products, or a publishing platform has millions of posts. An attacker repeatedly sends nonexistent IDs to the resource detail API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without a Bloom filter:&lt;/strong&gt; product/post request → Redis cache miss → database ID lookup → no data → 404. When IDs constantly change, cached entries for existing resources do not reduce these queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a Bloom filter:&lt;/strong&gt; request → check the filter for the resource type → return 404 immediately on a negative result; otherwise continue to the cache and database. Each resource type has its own ID set, preventing a product filter from being used to check post IDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; fewer queries to check nonexistent resources, leaving database capacity for legitimate product and post retrieval. Existing resources that are not cached still require database reads. Deleted resources whose IDs remain in the filter may still pass through and require verification against the source of truth.&lt;/p&gt;

&lt;p&gt;For files, the workflow is similar: stop on a negative result; on a positive result, look up metadata, check permissions, and retrieve the file from storage. The filter can reduce unnecessary metadata lookups, but it does not confirm that the file is still present in storage.&lt;/p&gt;

&lt;p&gt;In a multi-tenant system, include the tenant in the membership key, such as &lt;code&gt;tenant:42:product:123&lt;/code&gt;, to distinguish namespaces. A positive result only permits the lookup to continue; access control remains a separate requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.3. Checking Username Availability During Registration
&lt;/h3&gt;

&lt;p&gt;An application lets users check username availability during registration. Users try new names, or bots submit large numbers of unused names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without a Bloom filter:&lt;/strong&gt; each availability check consults an exact data store to determine whether the username exists, unless the result is already cached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a Bloom filter:&lt;/strong&gt; normalize the username → check the registered-name set → skip the duplicate-check read on a negative result; on a positive result, query the database to distinguish an existing name from a false positive. Do not report that a name is taken based only on a positive result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; fewer availability-check reads for unused names. Account creation still writes to the database; the filter does not reduce write costs or guarantee that the name remains available when the account is saved.&lt;/p&gt;

&lt;p&gt;Two requests can still attempt to register the same name simultaneously. A &lt;strong&gt;database unique constraint&lt;/strong&gt; must enforce uniqueness. A Bloom filter does not replace that constraint.&lt;/p&gt;

&lt;p&gt;Apply consistent username normalization during insertion and lookup, including case handling, Unicode handling, and product-specific rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.4. Checking Previously Crawled URLs or Processed Events
&lt;/h3&gt;

&lt;p&gt;A crawler receives new URLs, or an event-processing system needs to know whether an event ID has already been handled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without a Bloom filter:&lt;/strong&gt; before processing, the system reads its history store for every URL or event ID to check for duplicates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a Bloom filter:&lt;/strong&gt; check the recorded-ID set → on a negative result, skip the history-check read and proceed to recording/processing; on a positive result, consult the exact store to confirm a duplicate. Recording must still handle concurrent requests correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; fewer history reads when most URLs or events are new. If most have already been recorded, the filter usually returns positive and saves fewer reads. Recording and processing costs remain.&lt;/p&gt;

&lt;p&gt;Discarding an event based solely on a positive result could lose a legitimate event because of a false positive. For payments or other workflows requiring exact handling, use idempotency keys and storage mechanisms with appropriate guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Considerations Before Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7.1. Synchronizing New Users
&lt;/h3&gt;

&lt;p&gt;Suppose the database has just committed &lt;code&gt;user:2001&lt;/code&gt;, but the filter update is delayed. A request arriving during that window could incorrectly receive 404.&lt;/p&gt;

&lt;p&gt;One way to reduce this window is to add the ID to the filter before the record becomes visible to readers. If the database write fails, the extra ID in the filter merely causes unnecessary lookups. However, updating the filter first does not resolve every issue: cached 404 responses, rollbacks, filter loss, and replica lag still need handling.&lt;/p&gt;

&lt;p&gt;If updates are asynchronous through an outbox or CDC, provide a way to bypass the filter or directly check newly created data until synchronization completes. Monitoring lag helps detect problems, but does not itself guarantee the correctness of every negative result.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.2. Initial Loading and Rebuilding
&lt;/h3&gt;

&lt;p&gt;With 10 million users, read IDs in batches and build a new filter that does not yet serve traffic. Capture writes occurring during the scan, and switch to the new version only after loading and synchronization complete through a consistent process.&lt;/p&gt;

&lt;p&gt;Do not enable negative-result blocking while the dataset is only partially loaded. Bypass the filter if its version, readiness, or completeness cannot be trusted.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.3. Deleting Users
&lt;/h3&gt;

&lt;p&gt;A standard Bloom filter does not support deleting an individual item by clearing its bits, because multiple IDs may share them. A deleted user can still produce a positive result, followed by a database lookup that finds no record.&lt;/p&gt;

&lt;p&gt;Periodic rebuilding can remove old IDs. If frequent deletion is required, consider a structure that supports it and evaluate that structure's guarantees separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.4. Negative Caching and Rate Limiting
&lt;/h3&gt;

&lt;p&gt;Caching “not found” results with a short TTL reduces lookups for repeated IDs, including those producing false positives. Distinguish a negative cache entry from a cache miss, and invalidate it when the resource is created.&lt;/p&gt;

&lt;p&gt;Bloom filters help with many different nonexistent IDs. Negative caching helps with repeated IDs. Rate limiting remains necessary because rejected requests still consume application resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.5. Measuring Effectiveness
&lt;/h3&gt;

&lt;p&gt;Track the negative-result rate, database query volume, latency, filter size, and update lag. Positive results followed by missing records in the source of truth can reflect deleted IDs or synchronization issues as well as false positives; do not automatically treat them all as pure filter false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. When Should You Use a Bloom Filter?
&lt;/h2&gt;

&lt;p&gt;Consider a Bloom filter when the ID set is large, many lookups target nonexistent data, downstream lookups are costly, and the system can maintain a complete filter.&lt;/p&gt;

&lt;p&gt;If most requests target existing users, the filter may simply add CPU work or an extra Redis round trip. Measure it against a realistic workload before deploying broadly.&lt;/p&gt;

&lt;p&gt;In the API workflow, use the Bloom filter as a screening step: &lt;strong&gt;a trustworthy negative result lets you skip a lookup; a positive result still requires verification against an exact data source&lt;/strong&gt;. The database determines whether the data exists, and the application determines whether the caller may access it.&lt;/p&gt;

&lt;p&gt;I hope this article gives you more ideas for answering system design questions.&lt;/p&gt;

</description>
      <category>chores</category>
    </item>
    <item>
      <title>Deep Dive: Why SKIP LOCKED Requires READ COMMITTED in MySQL and PostgreSQL</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Thu, 08 Oct 2026 08:57:46 +0000</pubDate>
      <link>https://dev.to/quangpv/deep-dive-why-skip-locked-requires-read-committed-in-mysql-and-postgresql-2mil</link>
      <guid>https://dev.to/quangpv/deep-dive-why-skip-locked-requires-read-committed-in-mysql-and-postgresql-2mil</guid>
      <description>&lt;p&gt;Common Concurrency Interview Question: Handling High-Traffic Purchases and Key Approaches&lt;/p&gt;

&lt;p&gt;In technical and system design interviews, one of the most frequently asked questions is: “How do you handle thousands of users attempting to purchase the same product at the exact same second without overselling or crashing the database?” This article walks through the common approaches to this problem—including patterns&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp7xwrsivcnqgv2q16ej3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp7xwrsivcnqgv2q16ej3.jpg" alt="Deep Dive: Why SKIP LOCKED Requires READ COMMITTED in MySQL and PostgreSQL" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
](&lt;a href="https://dev.to/quangpv/common-concurrency-interview-question-handling-high-traffic-purchases-and-key-approaches-1d7i-temp-slug-5727368"&gt;https://dev.to/quangpv/common-concurrency-interview-question-handling-high-traffic-purchases-and-key-approaches-1d7i-temp-slug-5727368&lt;/a&gt;)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj98ts0uikxxc701fgrah.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj98ts0uikxxc701fgrah.jpg" alt="Deep Dive: Why SKIP LOCKED Requires READ COMMITTED in MySQL and PostgreSQL" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our previous article, we discussed how the Reservation Pool pattern eliminates hot-row contention by allowing concurrent transactions to bypass locked records.&lt;/p&gt;

&lt;p&gt;However, when engineers deploy &lt;code&gt;SKIP LOCKED&lt;/code&gt; to production or run their first high-concurrency load test, they frequently encounter an unexpected surprise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In MySQL: Transactions crash with &lt;code&gt;Deadlock found when trying to get lock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In PostgreSQL: Transactions abort with &lt;code&gt;ERROR: could not serialize access due to concurrent update&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wasn't &lt;code&gt;SKIP LOCKED&lt;/code&gt; designed specifically to eliminate lock waiting? Why would it trigger deadlocks or serialization errors?&lt;/p&gt;

&lt;p&gt;The issue is not &lt;code&gt;SKIP LOCKED&lt;/code&gt; itself. The culprit is the database engine's &lt;strong&gt;Transaction Isolation Level&lt;/strong&gt; —specifically, running under the default &lt;code&gt;REPEATABLE READ&lt;/code&gt;. This deep dive examines the internal database engine mechanics behind both engines and explains why switching to &lt;code&gt;READ COMMITTED&lt;/code&gt; is mandatory.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  1. The Core Conflict: Repeatable Read vs. Non-Blocking Traversal
&lt;/h2&gt;

&lt;p&gt;The isolation level &lt;code&gt;REPEATABLE READ&lt;/code&gt; is designed to guarantee that any data read during a transaction remains consistent throughout its lifecycle, preventing phenomena like Non-repeatable Reads and Phantom Reads.&lt;/p&gt;

&lt;p&gt;To provide these strong guarantees, database engines employ specialized locking and snapshot mechanics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MySQL (InnoDB):&lt;/strong&gt; Uses &lt;strong&gt;Next-Key Locks&lt;/strong&gt; (Record Locks combined with Gap Locks) to physically block inserts into scanned index intervals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL:&lt;/strong&gt; Uses &lt;strong&gt;Snapshot Isolation&lt;/strong&gt; , freezing a read snapshot at transaction start.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under high-concurrency reservation workflows, these exact mechanisms directly counteract the non-blocking intent of &lt;code&gt;SKIP LOCKED&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. MySQL (InnoDB): Gap Locks and Deadlock Cycles
&lt;/h2&gt;
&lt;h3&gt;
  
  
  2.1. What is a Gap Lock?
&lt;/h3&gt;

&lt;p&gt;In InnoDB, &lt;code&gt;REPEATABLE READ&lt;/code&gt; prevents phantom reads by placing locks not only on physical records, but also on the &lt;strong&gt;empty intervals (gaps) between them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;$$\text{Next-Key Lock} = \text{Record Lock} + \text{Gap Lock}$$&lt;/p&gt;

&lt;p&gt;For example, consider a table &lt;code&gt;reservation_pool&lt;/code&gt; with IDs &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;5&lt;/code&gt;, and &lt;code&gt;10&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Record 1] ─── (Gap 1 to 5) ─── [Record 5] ─── (Gap 5 to 10) ─── [Record 10]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a query scans index record &lt;code&gt;id = 1&lt;/code&gt;, InnoDB not only locks record &lt;code&gt;1&lt;/code&gt;, but also places a Gap Lock on the interval &lt;code&gt;(1, 5)&lt;/code&gt; to prevent other transactions from inserting rows between 1 and 5.&lt;/p&gt;




&lt;h3&gt;
  
  
  2.2. The Conflict with SKIP LOCKED
&lt;/h3&gt;

&lt;p&gt;The expected behavior of &lt;code&gt;SKIP LOCKED&lt;/code&gt; is simple: if record 1 is locked, skip it immediately and lock record 5.&lt;/p&gt;

&lt;p&gt;However, under &lt;code&gt;REPEATABLE READ&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwzgo1oy18t91o0bdfexz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwzgo1oy18t91o0bdfexz.png" alt="Deep Dive: Why SKIP LOCKED Requires READ COMMITTED in MySQL and PostgreSQL" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transaction B is prevented from advancing because interval &lt;code&gt;(1, 5)&lt;/code&gt; is gap-locked by Transaction A.&lt;/li&gt;
&lt;li&gt;When hundreds of transactions scan adjacent slots simultaneously, their Gap Locks overlap in a circular dependency, forcing InnoDB to abort transactions with: &lt;code&gt;Deadlock found when trying to get lock&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2.3. The Resolution: Switching to READ COMMITTED
&lt;/h3&gt;

&lt;p&gt;When you configure the transaction to run under &lt;strong&gt;&lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;InnoDB completely disables Gap Locking for index searches (retaining gap locks only for foreign key and duplicate-key checks).&lt;/li&gt;
&lt;li&gt;Record locks remain active, but the gaps between records are free.&lt;/li&gt;
&lt;li&gt;Transaction B detects that &lt;code&gt;id = 1&lt;/code&gt; holds a Record Lock, skips it, and locks &lt;code&gt;id = 5&lt;/code&gt; without blocking.&lt;/li&gt;
&lt;li&gt;All concurrent transactions proceed in parallel without deadlocks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. PostgreSQL: Snapshot Isolation and Serialization Failures
&lt;/h2&gt;

&lt;p&gt;PostgreSQL does not use gap locks, and its system default is already &lt;code&gt;READ COMMITTED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, if an application or connection pool is explicitly configured to &lt;strong&gt;&lt;code&gt;REPEATABLE READ&lt;/code&gt;&lt;/strong&gt; , &lt;code&gt;SKIP LOCKED&lt;/code&gt; fails under a different mechanism: &lt;strong&gt;Snapshot Isolation Conflict&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  3.1. How Snapshot Isolation Works in PostgreSQL
&lt;/h3&gt;

&lt;p&gt;Under &lt;code&gt;REPEATABLE READ&lt;/code&gt;, PostgreSQL takes a snapshot of the database at the start of the transaction ($T_0$). The transaction operates in an immutable view of the world: any writes committed by other transactions after $T_0$ are completely invisible to it.&lt;/p&gt;




&lt;h3&gt;
  
  
  3.2. Why REPEATABLE READ Aborts Concurrent Queries
&lt;/h3&gt;

&lt;p&gt;Consider two checkout transactions executing concurrently:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzzztpha7lnegjiw8ln8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzzztpha7lnegjiw8ln8.png" alt="Deep Dive: Why SKIP LOCKED Requires READ COMMITTED in MySQL and PostgreSQL" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sequence of events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Transaction B's frozen snapshot at $T_0$ evaluates Slot 1 as &lt;code&gt;available&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Because Transaction A already committed at $T_1$, Slot 1 currently has no active lock. Therefore, &lt;code&gt;SKIP LOCKED&lt;/code&gt; does not bypass Slot 1.&lt;/li&gt;
&lt;li&gt;When Transaction B attempts to lock Slot 1, PostgreSQL inspects the tuple on disk and discovers that it was updated and committed by Transaction A after Transaction B's snapshot point ($T_1 &amp;gt; T_0$).&lt;/li&gt;
&lt;li&gt;Under &lt;code&gt;REPEATABLE READ&lt;/code&gt;, PostgreSQL cannot allow Transaction B to view the new version (violating repeatable read semantics), nor can it allow Transaction B to overwrite Transaction A's committed changes.&lt;/li&gt;
&lt;li&gt;PostgreSQL immediately aborts Transaction B with:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: could not serialize access due to concurrent update

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3.3. The Resolution: READ COMMITTED and EvalPlanQual
&lt;/h3&gt;

&lt;p&gt;Under &lt;strong&gt;&lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each SQL statement takes a fresh snapshot at the moment it begins execution.&lt;/li&gt;
&lt;li&gt;PostgreSQL utilizes its internal &lt;code&gt;EvalPlanQual&lt;/code&gt; mechanism:

&lt;ul&gt;
&lt;li&gt;When the engine encounters Slot 1 (modified by Transaction A), it fetches the latest committed version.&lt;/li&gt;
&lt;li&gt;It re-evaluates the &lt;code&gt;WHERE status = 'available'&lt;/code&gt; predicate against the latest version.&lt;/li&gt;
&lt;li&gt;Because the latest version is now &lt;code&gt;status = 'reserved'&lt;/code&gt;, it no longer matches the predicate.&lt;/li&gt;
&lt;li&gt;PostgreSQL cleanly advances to Slot 2 without raising an error.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. Summary Matrix: MySQL vs. PostgreSQL
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technical Metric&lt;/th&gt;
&lt;th&gt;MySQL (InnoDB)&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default Isolation Level&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;REPEATABLE READ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gap Locking Mechanism&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Active in &lt;code&gt;REPEATABLE READ&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not implemented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Behavior under &lt;code&gt;REPEATABLE READ&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blocked on index gaps; causes Deadlocks&lt;/td&gt;
&lt;td&gt;Aborts with serialization failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Behavior under &lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Disables gap locks; skips locked rows cleanly&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;EvalPlanQual&lt;/code&gt;; advances to next row cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recommended Configuration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Explicitly set to &lt;code&gt;READ COMMITTED&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Retain default &lt;code&gt;READ COMMITTED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  5. Production Configuration Rules
&lt;/h2&gt;

&lt;p&gt;When implementing Reservation Pools or database-backed task queues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Always explicitly define the isolation level&lt;/strong&gt; at the transaction level rather than relying on global server defaults:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Index Alignment:&lt;/strong&gt; Ensure the &lt;code&gt;reservation_pool&lt;/code&gt; table has a composite index on &lt;code&gt;(product_id, status)&lt;/code&gt; so the engine evaluates only matching records without broad table scans.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. Conclusion
&lt;/h2&gt;

&lt;p&gt;In high-concurrency architectures, choosing the right SQL syntax is only half the battle. Understanding how your database engine handles index locks and snapshot versions is what determines whether your system scales or deadlocks under pressure.&lt;/p&gt;

&lt;p&gt;By pairing &lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt; with &lt;code&gt;READ COMMITTED&lt;/code&gt;, you remove the underlying causes of both MySQL Gap Lock deadlocks and PostgreSQL snapshot serialization conflicts, achieving true non-blocking parallel execution.&lt;/p&gt;

</description>
      <category>chores</category>
      <category>database</category>
    </item>
    <item>
      <title>Common Concurrency Interview Question: Handling High-Traffic Purchases and Key Approaches</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Thu, 08 Oct 2026 08:38:41 +0000</pubDate>
      <link>https://dev.to/quangpv/common-concurrency-interview-question-handling-high-traffic-purchases-and-key-approaches-3gap</link>
      <guid>https://dev.to/quangpv/common-concurrency-interview-question-handling-high-traffic-purchases-and-key-approaches-3gap</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspmc6fa7j8yz3abj3ij2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspmc6fa7j8yz3abj3ij2.jpg" alt="Common Concurrency Interview Question: Handling High-Traffic Purchases and Key Approaches" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In technical and system design interviews, one of the most frequently asked questions is: &lt;strong&gt;"How do you handle thousands of users attempting to purchase the same product at the exact same second without overselling or crashing the database?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article walks through the common approaches to this problem—including patterns you might already be using in production—evaluating the advantages and trade-offs of each, and presenting an optimal solution using the &lt;strong&gt;Reservation Pool Pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note on Redis: You could introduce an in-memory Redis cluster to decrement stock counters using atomic operations. However, from my perspective, introducing additional infrastructure brings substantial operational overhead, higher hosting costs, and added maintenance burden. Furthermore, it introduces distributed dual-write complexity and difficult rollback handling between cache and database. For these reasons, this article focuses strictly on solving the problem end-to-end within a single relational database transaction).&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Approach 1: DB Constraint (CHECK &amp;gt; 0)
&lt;/h2&gt;

&lt;p&gt;In this approach, you avoid explicit locking (&lt;code&gt;FOR UPDATE&lt;/code&gt;) and instead embed the condition directly into the &lt;code&gt;UPDATE&lt;/code&gt; statement alongside a database-level integrity constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Database-level integrity constraint&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;chk_stock_positive&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Atomic update statement&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application checks the result: if &lt;code&gt;affected rows &amp;gt; 0&lt;/code&gt;, the purchase succeeded; if 0, the item is out of stock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple to implement with no changes to table structure.&lt;/li&gt;
&lt;li&gt;Guarantees zero overselling: The &lt;code&gt;CHECK&lt;/code&gt; constraint and &lt;code&gt;WHERE stock &amp;gt; 0&lt;/code&gt; condition enforce data integrity at the database engine level.&lt;/li&gt;
&lt;li&gt;Eliminates an initial &lt;code&gt;SELECT&lt;/code&gt; round-trip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implicit Row Lock:&lt;/strong&gt; The &lt;code&gt;UPDATE&lt;/code&gt; statement still acquires an exclusive lock on that row. Thousands of requests remain serialized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection Pool Saturation:&lt;/strong&gt; While waiting for the row lock to release, waiting requests hold active database connections, leading to connection pool exhaustion during traffic spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tail Latency on Stockout:&lt;/strong&gt; If 100 items remain and 5,000 requests arrive, the last 4,900 requests queue sequentially just to eventually receive &lt;code&gt;affected rows = 0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Approach 2: Row-Level Lock (FOR UPDATE)
&lt;/h2&gt;

&lt;p&gt;The traditional approach explicitly locks the inventory record before executing application logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;START&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Application checks: if stock &amp;gt; 0, decrement inventory&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforces strict serializability.&lt;/li&gt;
&lt;li&gt;Allows application code to execute complex business rules and validations before committing the change.&lt;/li&gt;
&lt;li&gt;Well-suited for single-user idempotency (e.g., preventing duplicate orders when a user clicks the checkout button multiple times due to network delay).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Severe Hot-Row Contention:&lt;/strong&gt; When thousands of distinct users compete for the exact same record, all transactions queue sequentially behind a single row lock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prolonged Connection Retention:&lt;/strong&gt; Connection holding time grows linearly with queue depth, quickly depleting the connection pool and causing timeout errors across unrelated services.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Approach 3: Reservation Pool &amp;amp; SKIP LOCKED
&lt;/h2&gt;

&lt;p&gt;Instead of directing all traffic to a single row, this approach partitions available stock into discrete records (a pool of slots) and allows transactions to claim slots concurrently using &lt;code&gt;SKIP LOCKED&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. Workflow &amp;amp; Architecture
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkk000p04vusrinnktm3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkk000p04vusrinnktm3t.png" alt="Common Concurrency Interview Question: Handling High-Traffic Purchases and Key Approaches" width="799" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2. Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Master ledger tracking total counts&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;inventory_ledger&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_stock&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sold_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Reservation pool where each record represents an allocatable slot&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reservation_pool&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'available'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reserved'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sold'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'available'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reserved_by&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_product_status&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transaction execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt; &lt;span class="k"&gt;ISOLATION&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;READ&lt;/span&gt; &lt;span class="k"&gt;COMMITTED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;START&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Claim 1 available slot, automatically bypassing slots locked by active transactions&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reservation_pool&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SNEAKER-99'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'available'&lt;/span&gt; 
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; 
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Update the claimed slot to reserved&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;reservation_pool&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'reserved'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reserved_by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USER_101'&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;slot_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Increment sold counter in master ledger&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory_ledger&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;sold_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sold_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SNEAKER-99'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel Execution:&lt;/strong&gt; Transactions do not block one another. Subsequent transactions automatically skip locked slots and claim the next available row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal Connection Holding Time:&lt;/strong&gt; Each transaction holds a connection for only 2ms to 5ms before returning it to the pool, preventing connection exhaustion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant Stockout Detection:&lt;/strong&gt; When no slots remain, the query returns an empty result set immediately without queue delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100% ACID Consistency:&lt;/strong&gt; All operations complete within a single database transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires schema adjustments and an additional pool table.&lt;/li&gt;
&lt;li&gt;Requires a replenishment mechanism (e.g., maintaining a buffer of 500 to 1,000 active rows from the ledger rather than pre-generating rows for the entire catalog).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Isolation Level: Why READ COMMITTED
&lt;/h2&gt;

&lt;p&gt;When implementing &lt;code&gt;SKIP LOCKED&lt;/code&gt;, configuring the transaction isolation level is mandatory: &lt;strong&gt;it must run under &lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The key differences between database engines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MySQL (InnoDB):&lt;/strong&gt; The default isolation level is &lt;code&gt;REPEATABLE READ&lt;/code&gt;. Under this level, InnoDB places &lt;strong&gt;Gap Locks&lt;/strong&gt; across index intervals. When multiple transactions scan for slots concurrently, overlapping Gap Locks block one another, neutralizing &lt;code&gt;SKIP LOCKED&lt;/code&gt; and causing &lt;strong&gt;Deadlocks&lt;/strong&gt;. Switching to &lt;code&gt;READ COMMITTED&lt;/code&gt; disables gap locking for index scans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL:&lt;/strong&gt; The default is already &lt;code&gt;READ COMMITTED&lt;/code&gt;. If set to &lt;code&gt;REPEATABLE READ&lt;/code&gt;, PostgreSQL uses Snapshot Isolation and aborts transactions with &lt;code&gt;ERROR: could not serialize access due to concurrent update&lt;/code&gt; when a row is modified by another committed transaction. Under &lt;code&gt;READ COMMITTED&lt;/code&gt;, PostgreSQL's internal &lt;code&gt;EvalPlanQual&lt;/code&gt; mechanism automatically re-evaluates the latest row version and skips locked or committed rows cleanly without errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;For an in-depth database engine analysis, refer to:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;Deep Dive: Why SKIP LOCKED Requires READ COMMITTED in MySQL and PostgreSQL&lt;/p&gt;

&lt;p&gt;Common Concurrency Interview Question: Handling High-Traffic Purchases and Key ApproachesIn technical and system design interviews, one of the most frequently asked questions is: “How do you handle thousands of users attempting to purchase the same product at the exact same second without overselling or crashing the database?” This article walks&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxgqklpkenvgw4zt2s13c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxgqklpkenvgw4zt2s13c.png" alt="Common Concurrency Interview Question: Handling High-Traffic Purchases and Key Approaches" width="256" height="256"&gt;&lt;/a&gt;Quang's Tech CornerQuangPV&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkyev0n1d1xmdd424lyas.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkyev0n1d1xmdd424lyas.jpg" alt="Common Concurrency Interview Question: Handling High-Traffic Purchases and Key Approaches" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
](&lt;a href="https://blog.quangpv.top/deep-dive-why-skip-locked-requires-read-committed-in-mysql-and-postgresql/" rel="noopener noreferrer"&gt;https://blog.quangpv.top/deep-dive-why-skip-locked-requires-read-committed-in-mysql-and-postgresql/&lt;/a&gt;)&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Summary Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evaluation Metric&lt;/th&gt;
&lt;th&gt;Approach 1: Constraint &lt;code&gt;&amp;gt; 0&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Approach 2: Row Lock (&lt;code&gt;FOR UPDATE&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Approach 3: Pool Pattern + &lt;code&gt;SKIP LOCKED&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Locking Mechanism&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Implicit lock on &lt;code&gt;UPDATE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Explicit exclusive row lock&lt;/td&gt;
&lt;td&gt;Row lock on individual slot; skips busy rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serialized (Blocking)&lt;/td&gt;
&lt;td&gt;Serialized (Blocking)&lt;/td&gt;
&lt;td&gt;Parallel (Non-blocking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connection Holding Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Increases with queue depth&lt;/td&gt;
&lt;td&gt;Increases with queue depth&lt;/td&gt;
&lt;td&gt;Minimal (2ms – 5ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connection Pool Risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High during traffic surges&lt;/td&gt;
&lt;td&gt;High during traffic surges&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Out-of-Stock Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Must wait through queue&lt;/td&gt;
&lt;td&gt;Must wait through queue&lt;/td&gt;
&lt;td&gt;Instantaneous (empty result in ~2ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Implementation Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  6. Conclusion
&lt;/h2&gt;

&lt;p&gt;Hopefully, these approaches give you broader perspectives on handling concurrency trade-offs and provide a clear edge in your technical interviews.&lt;/p&gt;

&lt;p&gt;Being able to explain when a simple constraint is sufficient versus when an advanced pattern like a Reservation Pool with &lt;code&gt;SKIP LOCKED&lt;/code&gt; is required demonstrates practical engineering maturity under real-world constraints.&lt;/p&gt;

</description>
      <category>chores</category>
      <category>database</category>
    </item>
    <item>
      <title>Realtime Config Updates for Multiple Instances with Redis Pub/Sub</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Mon, 18 May 2026 06:54:35 +0000</pubDate>
      <link>https://dev.to/quangpv/realtime-config-updates-for-multiple-instances-with-redis-pubsub-35b6</link>
      <guid>https://dev.to/quangpv/realtime-config-updates-for-multiple-instances-with-redis-pubsub-35b6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3g4jj3a9xv637xndtyu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3g4jj3a9xv637xndtyu.png" alt="Realtime Config Updates for Multiple Instances with Redis Pub/Sub" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I want to share one way to handle dynamic configuration updates with Redis Pub/Sub.&lt;/p&gt;

&lt;p&gt;But before getting into the implementation, I think it is worth stepping back and asking a more basic question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When configuration can change while the system is running, where should the application read that configuration from?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are a few common approaches, and each comes with its own trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Ways to Handle Dynamic Configuration
&lt;/h2&gt;

&lt;p&gt;When configuration can change at runtime, one natural approach is to store it in a database or Redis DB.&lt;/p&gt;

&lt;p&gt;For example, whenever the application needs to know whether it should send notifications to Slack or Telegram, it reads configuration from a DBMS, Redis DB, or a config service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;telegram:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;slack:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The clear advantage of this approach is that the application always reads the latest value.&lt;/p&gt;

&lt;p&gt;The downside is that every request, or every time some piece of logic runs, the application has to hit the DBMS or Redis to fetch configuration. If that configuration is read frequently but changes rarely, a relatively static piece of data becomes a repeated access point in the system.&lt;/p&gt;

&lt;p&gt;Another common approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the configuration once when the application starts.&lt;/li&gt;
&lt;li&gt;Keep it in memory inside each instance.&lt;/li&gt;
&lt;li&gt;When the configuration changes, update the source of truth and restart the instances so they reload the new value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This avoids touching the database or Redis on every request. But once the number of instances grows, restarting stops being an invisible operation. It becomes part of the operational workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;restart in the right order,&lt;/li&gt;
&lt;li&gt;make sure no instance is missed,&lt;/li&gt;
&lt;li&gt;accept a delay before the whole system becomes consistent again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Another Approach: Keep Config in Memory and Use Redis Pub/Sub for Realtime Updates
&lt;/h2&gt;

&lt;p&gt;Instead of reading from the DB or Redis every time configuration is needed, I can keep the configuration in memory inside each instance for fast access.&lt;/p&gt;

&lt;p&gt;The remaining question is: when an admin updates the configuration, how do all running instances know that they should refresh their local copy?&lt;/p&gt;

&lt;p&gt;This is where Redis Pub/Sub is useful.&lt;/p&gt;

&lt;p&gt;Instead of making every instance poll for changes, I let them listen for a shared event.&lt;/p&gt;

&lt;p&gt;The flow is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Admin updates config
        |
        v
Publish event to Redis channel
        |
        v
All subscribed instances receive the event
        |
        v
Each instance updates its in-memory config

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefits are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no application restart required,&lt;/li&gt;
&lt;li&gt;no need to hit the DBMS or Redis DB every time configuration is read,&lt;/li&gt;
&lt;li&gt;no continuous polling,&lt;/li&gt;
&lt;li&gt;multiple instances can receive the update at nearly the same time,&lt;/li&gt;
&lt;li&gt;in-memory configuration can be refreshed immediately after an event arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redis Pub/Sub is not the source of truth for configuration here.&lt;br&gt;&lt;br&gt;
It acts more like a bell: “the config changed, refresh your local copy.”&lt;/p&gt;
&lt;h2&gt;
  
  
  Example with NestJS
&lt;/h2&gt;

&lt;p&gt;Suppose the system can send notifications to Telegram and Slack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpepl5iwxt8kcm6n0178a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpepl5iwxt8kcm6n0178a.gif" alt="Realtime Config Updates for Multiple Instances with Redis Pub/Sub" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The initial configuration may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;telegram:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;slack:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, an admin wants to disable Slack notifications while keeping Telegram enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;telegram:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;slack:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, I usually do not create a separate Redis client for every service.&lt;/p&gt;

&lt;p&gt;Redis should be treated as shared application infrastructure, so multiple features can reuse the same publisher and listener:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one global &lt;code&gt;RedisPublisher&lt;/code&gt; to publish events,&lt;/li&gt;
&lt;li&gt;one global &lt;code&gt;RedisListener&lt;/code&gt; to subscribe and dispatch messages to interested modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Global Redis Publisher and Listener
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OnModuleInit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RUNTIME_CONFIG_CHANNEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;runtime-config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RedisPublisher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RedisListener&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;OnModuleInit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;emitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventEmitter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;onModuleInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RUNTIME_CONFIG_CHANNEL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. API for Updating Notification Configuration
&lt;/h3&gt;

&lt;p&gt;Here I use the key &lt;code&gt;notification.channels&lt;/code&gt;, because this configuration belongs specifically to the notification feature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Put&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NotificationChannelsConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;telegram&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin/notification-config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationConfigController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;redisPublisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RedisPublisher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NotificationChannelsConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redisPublisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RUNTIME_CONFIG_CHANNEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notification.channels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the admin calls the API with this payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;telegram:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;slack:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the published message carries the updated configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;NotificationService&lt;/code&gt; Keeps Config in Memory
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;NotificationService&lt;/code&gt; has a &lt;code&gt;config&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;Its default value is loaded from &lt;code&gt;.env&lt;/code&gt;. When the module initializes, the service registers with the global Redis listener and only reacts to the key it cares about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OnModuleInit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NotificationChannelsConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;telegram&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationService&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;OnModuleInit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NotificationChannelsConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;telegram&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTIFY_TELEGRAM&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTIFY_SLACK&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;redisListener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RedisListener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;onModuleInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redisListener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RUNTIME_CONFIG_CHANNEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NotificationChannelsConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notification.channels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;getConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telegram&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendToTelegram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendToSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sendToTelegram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// send telegram message&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sendToSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// send slack message&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point is that &lt;code&gt;notify()&lt;/code&gt; does not need to query the database or Redis every time it runs.&lt;br&gt;&lt;br&gt;
It only calls &lt;code&gt;getConfig()&lt;/code&gt; to read the configuration already available in the instance's memory.&lt;/p&gt;

&lt;p&gt;When the admin updates the configuration through the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;telegram:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;slack:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;all running instances receive the event, update &lt;code&gt;this.config&lt;/code&gt;, and from the next request onward, &lt;code&gt;notify()&lt;/code&gt; sends only to Telegram without requiring an application restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Important Note Before Going to Production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Redis publishers and subscribers can lose their connection, reconnect slowly, or be temporarily offline at the exact moment a message is published.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So in a real system, I would still want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automatic reconnect and retry logic for both publishers and subscribers,&lt;/li&gt;
&lt;li&gt;logs and metrics to know when Redis connections drop,&lt;/li&gt;
&lt;li&gt;a config source of truth such as a database, Redis key-value storage, or a config service so instances can load the correct configuration on startup or reconnect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, plain Redis Pub/Sub follows a &lt;strong&gt;fire-and-forget&lt;/strong&gt; model.&lt;br&gt;&lt;br&gt;
It does not provide ACKs or retry delivery automatically when a subscriber misses a message.&lt;/p&gt;

&lt;p&gt;If the configuration needs stronger guarantees, I can add an ACK mechanism:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each event carries an &lt;code&gt;eventId&lt;/code&gt; or &lt;code&gt;version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;After applying the update, the subscriber sends back an ACK for the applied version.&lt;/li&gt;
&lt;li&gt;If the publisher does not receive enough ACKs within a time window, it can retry or reconcile the state again.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;publish config.updated(version=42)
        |
        v
subscriber updates this.config
        |
        v
subscriber ACKs version=42
        |
        v
if ACK is missing -&amp;gt; retry / reconcile state

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In short:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Pub/Sub helps with realtime updates. Reconnects, ACKs, and retries are what make the system safer to run in production.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If a configuration value is read very often but changes only occasionally, keeping it in memory and using Redis Pub/Sub to synchronize updates across instances can be an efficient approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer DB/Redis reads,&lt;/li&gt;
&lt;li&gt;no need to restart the application every time configuration changes,&lt;/li&gt;
&lt;li&gt;near-realtime updates across instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is that I also need to manage an additional moving part in the system: Redis Pub/Sub itself, along with reconnect, retry, and ACK mechanisms when stronger reliability is required.&lt;/p&gt;

&lt;p&gt;I hope you found this article useful.&lt;/p&gt;

</description>
      <category>chores</category>
    </item>
    <item>
      <title>The Auto-Increment Trap: Scaling Systems and the Distributed ID Problem</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Fri, 24 Apr 2026 16:08:46 +0000</pubDate>
      <link>https://dev.to/quangpv/the-auto-increment-trap-scaling-systems-and-the-distributed-id-problem-2a54</link>
      <guid>https://dev.to/quangpv/the-auto-increment-trap-scaling-systems-and-the-distributed-id-problem-2a54</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgyg7szkmzx8p9pn61not.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgyg7szkmzx8p9pn61not.png" alt="The Auto-Increment Trap: Scaling Systems and the Distributed ID Problem" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the world of relational databases, &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; is like a trusted "old friend." Why do we love it so much?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Excellent Performance&lt;/strong&gt; : Since IDs increase sequentially, B-Tree indexes work extremely efficiently. New data is always inserted at the end of the page, avoiding index fragmentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Space Efficiency&lt;/strong&gt; : &lt;code&gt;INT&lt;/code&gt; or &lt;code&gt;BIGINT&lt;/code&gt; types take only 4-8 bytes, much smaller than UUIDs (16 bytes), which reduces storage size and speeds up scans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-Friendly&lt;/strong&gt; : Looking at &lt;code&gt;ID=123&lt;/code&gt; is much easier to read and debug than a long, messy string like &lt;code&gt;550e8400-e29b-41d4-a716-446655440000&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, this "friend" starts to get "cranky" when your system enters the Scale-out or High Availability (HA) phase. At this point, the initial convenience becomes an annoying technical hurdle.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The "Disaster" Scenario: When Slave is Promoted to Master
&lt;/h2&gt;

&lt;p&gt;In typical High Availability (HA) architectures, we usually have a Master-Slave cluster. All write operations are sent to the Master and then replicated to the Slaves.&lt;/p&gt;

&lt;p&gt;The problem arises when the Master fails and we need to "promote" a Slave to be the new Master.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the Bug?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Replication in many databases (like MySQL) is often &lt;em&gt;Asynchronous&lt;/em&gt; or &lt;em&gt;Semi-synchronous&lt;/em&gt;. When the Master writes a record with &lt;code&gt;ID=100&lt;/code&gt;, the Slave might only have received up to &lt;code&gt;ID=98&lt;/code&gt; before the Master goes down. When this Slave is promoted to Master, it will continue issuing IDs starting from &lt;code&gt;99&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; The system will attempt to insert records with &lt;code&gt;ID=99, 100&lt;/code&gt; — values that &lt;strong&gt;already exist&lt;/strong&gt; in business logic or related systems (like Cache, ElasticSearch, or even the old Master's backup files). A "Duplicate Entry" bug explodes, and data recovery becomes a nightmare due to primary key conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The "Step k" Solution: A Temporary but Risky Lifesaver
&lt;/h2&gt;

&lt;p&gt;To solve the issue of multiple instances writing without ID collisions, a classic technique is to set an &lt;strong&gt;Offset&lt;/strong&gt; and an &lt;strong&gt;Increment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suppose you have 2 DB Instances; you could configure them as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server A:&lt;/strong&gt; &lt;code&gt;auto_increment_offset = 1&lt;/code&gt;, &lt;code&gt;auto_increment_increment = 2&lt;/code&gt; (IDs: 1, 3, 5, 7...)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server B:&lt;/strong&gt; &lt;code&gt;auto_increment_offset = 2&lt;/code&gt;, &lt;code&gt;auto_increment_increment = 2&lt;/code&gt; (IDs: 2, 4, 6, 8...)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  MySQL Configuration Example:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- On Server 1&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;auto_increment_increment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Assuming overhead for up to 10 servers&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;auto_increment_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- On Server 2&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;auto_increment_increment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;auto_increment_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why is this solution hard to maintain?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Low Flexibility:&lt;/strong&gt; If you initially set the step &lt;code&gt;k=2&lt;/code&gt; (for 2 servers) and later need to scale to 5 servers, you must reconfigure &lt;strong&gt;all&lt;/strong&gt; existing instances. This is extremely risky and often requires downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed Management:&lt;/strong&gt; You must ensure that offsets never overlap between servers. In a cloud environment with dozens of microservices and DB instances, manual maintenance is painful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ID Waste:&lt;/strong&gt; If a server is down for a long period, the ID range reserved for it remains unused. While not a huge issue with &lt;code&gt;BIGINT&lt;/code&gt;, it represents a lack of optimization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Auto-increment and the Pain of "Sharding"
&lt;/h2&gt;

&lt;p&gt;When data becomes too massive, we must split the database into multiple "shards." At this point, Auto-increment is almost useless if you need a &lt;strong&gt;Globally Unique ID&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If each shard runs its own &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shard 1 has &lt;code&gt;User ID = 10&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Shard 2 also has &lt;code&gt;User ID = 10&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the application needs to perform "cross-shard" queries (e.g., getting friends of User 10, where those friends are scattered across multiple shards), the system won't know which "User 10" is which. Merging data from shards into a central warehouse (Data Warehouse) for analysis will also cause immediate Primary Key conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. More Suitable Alternatives
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; can no longer handle the load, consider these "candidates" based on your specific use case:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Twitter Snowflake (and its variants)&lt;/strong&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UUID v7 (The modern choice)&lt;/strong&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database Ticket Server (Flickr/Instagram approach)&lt;/strong&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Segment Technique (Meituan/Leaf approach)&lt;/strong&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In the end, every technical choice is a &lt;strong&gt;Trade-off&lt;/strong&gt;. There is no "silver bullet" for every situation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the project is small and needs speed: Stick with &lt;strong&gt;Auto-increment&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If you need HA and sequential ordering: &lt;strong&gt;UUID v7&lt;/strong&gt; or &lt;strong&gt;Snowflake&lt;/strong&gt; are excellent choices.&lt;/li&gt;
&lt;li&gt;If you want short IDs for a massive system: Consider the &lt;strong&gt;Ticket Server&lt;/strong&gt; or &lt;strong&gt;Segment model&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose the one that best fits your current context and your team's operational capabilities, rather than simply choosing the most complex one!&lt;/p&gt;

</description>
      <category>database</category>
    </item>
    <item>
      <title>Big O: Don't talk about speed!</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Fri, 24 Apr 2026 15:53:47 +0000</pubDate>
      <link>https://dev.to/quangpv/big-o-dont-talk-about-speed-3662</link>
      <guid>https://dev.to/quangpv/big-o-dont-talk-about-speed-3662</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokmqug25pp11u7dhfmst.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokmqug25pp11u7dhfmst.png" alt="Big O: Don't talk about speed!" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the world of computer science, we are often taught a simple hierarchy: (O(1) &amp;lt; O(log N) &amp;lt; O(N) &amp;lt; O(N log N) &amp;lt; O(N^2)). We are led to believe that an (O(N)) algorithm is inherently "better" than an (O(N^2)) one.&lt;/p&gt;

&lt;p&gt;However, in the real world, this is a dangerous oversimplification. &lt;strong&gt;Big O notation is a tool for classifying growth rates as (N) approaches infinity&lt;/strong&gt;, not a stopwatch for measuring absolute execution time for practical data sizes. One of the most common pitfalls in performance optimization is ignoring the "hidden constants" and the reality of modern hardware.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvj0dzrxukayjdqrna24y.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvj0dzrxukayjdqrna24y.jpg" alt="Big O: Don't talk about speed!" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Deception of Hidden Constants
&lt;/h2&gt;

&lt;p&gt;Let’s look at a concrete example. Suppose you are choosing between two algorithms to solve the same problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm A&lt;/strong&gt; : Complexity is (O(100,000,000 times N)) -&amp;gt; In Big O terms, we drop the constant, so it is &lt;strong&gt;(O(N))&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm B&lt;/strong&gt; : Complexity is (O(2 times N^2)) -&amp;gt; Dropping the constant, it is &lt;strong&gt;(O(N^2))&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper, a junior developer would choose Algorithm A without hesitation. But let’s see what happens when we run these algorithms with (N = 100):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm B&lt;/strong&gt; takes: (2 times 100^2 = 20,000) steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm A&lt;/strong&gt; takes: (100,000,000 times 100 = 10,000,000,000) (10 billion) steps!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With (N = 100), Algorithm B is &lt;strong&gt;500,000 times faster&lt;/strong&gt; than Algorithm A. While A is theoretically "better," it is practically unusable for small inputs due to its massive constant factor.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Crossover Point: Where Theory Meets Reality
&lt;/h2&gt;

&lt;p&gt;So, when does (O(N)) actually become faster? Only when (N) is &lt;strong&gt;large enough&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is known as the "Crossover Point." On a performance graph, the (O(N^2)) curve starts lower but grows quadratically. The (O(N)) curve starts much higher due to the constant but grows linearly. They eventually intersect at the crossover point.&lt;/p&gt;

&lt;p&gt;In our example, the crossover point is at (N = 50,000,000). Unless your dataset is larger than 50 million elements, the (O(N^2)) algorithm is actually the superior choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Hardware Reality: The Critical Cache Factor
&lt;/h2&gt;

&lt;p&gt;Big O assumes all operations are equal. In reality, hardware costs vary wildly. A single addition is fast, but a memory access can be a bottleneck. Specifically, reading from &lt;strong&gt;Main Memory (RAM)&lt;/strong&gt; can be &lt;strong&gt;100x slower&lt;/strong&gt; than reading from the &lt;strong&gt;CPU Cache (L1/L2)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This leads to a fascinating reality: An (O(N)) algorithm that jumps around randomly in memory (causing constant Cache Misses) can be significantly slower than an (O(N^2)) algorithm that accesses data sequentially (utilizing Cache Locality). The theoretical "fewer operations" of (O(N)) are completely wiped out by the massive latency of the hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Real-World Data &amp;amp; The Trap of Micro-Optimization
&lt;/h2&gt;

&lt;p&gt;Real-world data size is the most important factor. Before you complicate your codebase with advanced data structures just to reduce a Big O class, ask yourself: &lt;strong&gt;"How large is my data really?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your array rarely exceeds 100 elements, switching from a simple nested loop to a complex balanced tree might actually slow down your program due to setup overhead and poor cache usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prioritize benchmarking over theoretical complexity.&lt;/strong&gt; Use profiling tools to measure actual execution time with real-world data. Avoid "micro-performance" improvements that make the code unreadable without providing a measurable gain.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Final Conclusion: Developing a Pragmatic Perspective
&lt;/h2&gt;

&lt;p&gt;To summarize the key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Big O is a classification tool, not a measurement&lt;/strong&gt; : It describes how an algorithm scales, not how fast it runs on your machine today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't ignore constants and hardware&lt;/strong&gt; : Large constants or poor cache locality can make a "better" Big O class perform worse in practice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-world data is king&lt;/strong&gt; : Always measure and benchmark with actual data sizes before deciding to optimize. Avoid premature micro-optimization that adds unnecessary complexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding these nuances will give you a much sharper perspective on algorithm design. It provides you with solid arguments when &lt;strong&gt;debating with colleagues&lt;/strong&gt; during design reviews and helps you &lt;strong&gt;stand out to interviewers&lt;/strong&gt; by demonstrating a pragmatic, senior-level approach to performance engineering.&lt;/p&gt;

</description>
      <category>chores</category>
    </item>
    <item>
      <title>Distributed Locking &amp; The 'Old Overwrites New' Problem (Fencing Tokens)</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Wed, 22 Apr 2026 08:38:20 +0000</pubDate>
      <link>https://dev.to/quangpv/distributed-locking-the-old-overwrites-new-problem-fencing-tokens-n3l</link>
      <guid>https://dev.to/quangpv/distributed-locking-the-old-overwrites-new-problem-fencing-tokens-n3l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtmolhrii5853k5bclcr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtmolhrii5853k5bclcr.png" alt="Distributed Locking &amp;amp; The 'Old Overwrites New' Problem (Fencing Tokens)" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When designing Distributed Systems, we often use a &lt;strong&gt;Distributed Lock&lt;/strong&gt; to ensure that at any given time, only one process is allowed to perform a specific task. For example: writing data to a shared file, or deducting money in a database.&lt;/p&gt;

&lt;p&gt;However, there is a deadly trap. Even if you use the most sophisticated locking algorithms, your system can still break due to unintended data overwriting. Today, we will explore this problem and its solution: &lt;strong&gt;Fencing Tokens&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: The "Awakened" Node and Data Overwrites
&lt;/h2&gt;

&lt;p&gt;Typically, when using a Distributed Lock, our code looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// THIS CODE IS NOT NECESSARILY SAFE (UNSAFE)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;writeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lockService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquireLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to acquire lock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;updateContents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although the logic seems airtight, in a distributed system environment, the code above has a critical flaw. Let's look at 2 common cases that cause errors:&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 1: Node A pauses for too long
&lt;/h3&gt;

&lt;p&gt;Node A acquires the lock but is paused (e.g., due to Garbage Collection, network issues...) until the lock's Time To Live (TTL) expires. At this point, Node B sees the lock is available, jumps in to acquire it, and successfully writes its data. Later, Node A suddenly "wakes up", continues its remaining execution, and overwrites the result on top of the data Node B just wrote. Result: Node B's data is unintentionally overwritten by Node A.&lt;/p&gt;

&lt;h4&gt;
  
  
  Illustration: Case 1 (Data Overwrite due to Pause)
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FData-Overwrite-due-to-Pause.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FData-Overwrite-due-to-Pause.JPG" alt="Distributed Locking &amp;amp; The 'Old Overwrites New' Problem (Fencing Tokens)" width="668" height="760"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 2: Intersection Time Conflict (Race Condition)
&lt;/h3&gt;

&lt;p&gt;Even if Node A finishes processing before Node B (meaning Node B overwriting Node A would be logically correct sequentially), there is a problem. If Node A's TTL expires right as it is preparing to write, an "intersection" of time occurs between when Node A is finishing its operation (without the lock) and when Node B has just acquired the lock to start reading/processing. This resource overlap creates potential Race Condition bugs that are extremely hard to debug.&lt;/p&gt;

&lt;h4&gt;
  
  
  Illustration: Case 2 (Intersection Conflict)
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FIntersection-Conflict.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FIntersection-Conflict.JPG" alt="Distributed Locking &amp;amp; The 'Old Overwrites New' Problem (Fencing Tokens)" width="720" height="752"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the root cause is that we are &lt;strong&gt;relying solely on the client&lt;/strong&gt; to determine if it has permission to write. But in a distributed system, the time and state of the client are unreliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Fencing Tokens
&lt;/h2&gt;

&lt;p&gt;To completely solve this problem, Martin Kleppmann (author of &lt;em&gt;Designing Data-Intensive Applications&lt;/em&gt;) emphasizes the importance of &lt;strong&gt;Fencing Tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea here is: Every time a Node acquires a lock, the Lock Service doesn't just return "OK", it must also grant the Node a &lt;strong&gt;Token&lt;/strong&gt; (a monotonically increasing number).&lt;/p&gt;

&lt;p&gt;The process changes as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Node A&lt;/strong&gt; acquires the lock, receives Token = 33.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node A&lt;/strong&gt; is "frozen", lock expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node B&lt;/strong&gt; acquires the lock, receives Token = 34 (since the token number always increases).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node B&lt;/strong&gt; sends a write request along with Token: 34. The Storage Service records the highest current token as 34 and saves the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node A&lt;/strong&gt;"wakes up", sends a write request along with Token: 33.&lt;/li&gt;
&lt;li&gt;The Storage Service checks: 33 is less than 34 (Token is old). Node A's request is &lt;strong&gt;REJECTED&lt;/strong&gt;. Data is safe!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Solution Illustration (Fencing Token)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FFencing-Token.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FFencing-Token.JPG" alt="Distributed Locking &amp;amp; The 'Old Overwrites New' Problem (Fencing Tokens)" width="734" height="754"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Big Takeaway
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never completely trust the Client:&lt;/strong&gt; Time (Clocks), Network Delays, or process interruptions (GC Pauses) can trick the client into thinking it's still holding the lock safely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect from the Resource side (Storage):&lt;/strong&gt; The destination of the data (Storage, Database, External API...) must have a "Gating Mechanism" to verify the validity of the request. Fencing Tokens are a perfect mechanism for this.&lt;/li&gt;
&lt;li&gt;If you only need a Lock for &lt;strong&gt;Efficiency&lt;/strong&gt; optimization (avoiding duplicate work), the lock occasionally slipping might be okay. But if you need a Lock to ensure data &lt;strong&gt;Correctness&lt;/strong&gt; , lacking Fencing Tokens (like some simple locking algorithms) is a fatal flaw!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core principle:&lt;/strong&gt; When a task takes longer to process than the lock's TTL, that execution flow is inherently &lt;strong&gt;Unsafe&lt;/strong&gt;. Always be careful with the actual execution time of a task!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Reference: The article &lt;a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html?ref=blog.quangpv.top" rel="noopener noreferrer"&gt;How to do distributed locking&lt;/a&gt; by Martin Kleppmann.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>chores</category>
    </item>
    <item>
      <title>How CPU Cores, Processes, and Threads Actually Work</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Wed, 22 Apr 2026 08:15:42 +0000</pubDate>
      <link>https://dev.to/quangpv/how-cpu-cores-processes-and-threads-actually-work-2glj</link>
      <guid>https://dev.to/quangpv/how-cpu-cores-processes-and-threads-actually-work-2glj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19qcwbgt3xqag2j7o9em.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19qcwbgt3xqag2j7o9em.png" alt="How CPU Cores, Processes, and Threads Actually Work" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This document summarizes the core concepts of how a CPU handles processes and threads, the mechanism of time slicing along with its trade-offs, and how popular programming languages apply these models.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Basic Overview: CPU, Core, Process, and Thread
&lt;/h2&gt;

&lt;p&gt;To understand how a computer works, we go from hardware to software:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU (Central Processing Unit)&lt;/strong&gt;: The central processor. A computer can have one or multiple physical CPUs (sockets).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core&lt;/strong&gt; : A CPU contains multiple Cores. &lt;strong&gt;Each Core is an actual physical processing unit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process&lt;/strong&gt; : The "container" that holds a running program. Each process has its own completely independent memory space and resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thread&lt;/strong&gt; : The &lt;strong&gt;smallest unit of execution&lt;/strong&gt; that the Operating System (OS) can schedule. A process contains at least one thread (the main thread) and can contain multiple threads.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Core Truth:&lt;/strong&gt; The CPU does not run Processes — &lt;strong&gt;the CPU runs Threads&lt;/strong&gt;. A process is just an outer shell. Context switching between Processes is actually switching between Threads belonging to different Processes (which also entails switching memory spaces, making it very heavy).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FBasic-Overview.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FBasic-Overview.JPG" alt="How CPU Cores, Processes, and Threads Actually Work" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. CPU Processing Mechanism and Time Slicing
&lt;/h2&gt;

&lt;p&gt;Each Core can only run &lt;strong&gt;exactly 1 Thread&lt;/strong&gt; at any given time. So what happens when the number of active Threads exceeds the number of Cores?&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. True Parallelism
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;active Threads ≤ number of Cores&lt;/code&gt;:&lt;br&gt;&lt;br&gt;
Each Thread is assigned to a separate Core. Everything runs in true parallel at the same time. There is no waiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2. Time Slicing and Context Switch
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;active Threads &amp;gt; number of Cores&lt;/code&gt; (The most common scenario):&lt;br&gt;&lt;br&gt;
The CPU cannot process all of them simultaneously. The Operating System (OS) applies the &lt;strong&gt;Time Slicing&lt;/strong&gt; mechanism.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The OS divides CPU time into very small slices (called &lt;em&gt;Time Slices&lt;/em&gt; or &lt;em&gt;Quantums&lt;/em&gt;, usually just a few milliseconds).&lt;/li&gt;
&lt;li&gt;The OS alternately assigns the CPU to Thread A until its time slice runs out, then pauses A, switches to Thread B, then to Thread C, etc. The switching speed is so extremely fast that it gives us the illusion that everything is running "concurrently".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The process of switching from one Thread to another is called a &lt;strong&gt;Context Switch&lt;/strong&gt;. The OS must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save the state (Registers, Program Counter...) of the currently running Thread.&lt;/li&gt;
&lt;li&gt;Load the state of the next Thread to be run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Below is a Timeline diagram illustrating the cost of Time Slicing interleaved with Context Switches:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FTime-Slicing-and-Context-Switch.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FTime-Slicing-and-Context-Switch.JPG" alt="How CPU Cores, Processes, and Threads Actually Work" width="799" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Chart Note:&lt;/strong&gt; The red &lt;code&gt;[Overhead]&lt;/code&gt; blocks represent the "dead time" (cost) where the CPU does no useful work for the application other than unloading the old Thread and loading the new Thread.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. The Nature of Time Slicing and Trade-offs
&lt;/h2&gt;

&lt;p&gt;Time Slicing solves the multitasking problem, but it's not perfect. It brings massive hidden costs if abused.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. The Context Switch Paradox and Minimal Execution Time
&lt;/h3&gt;

&lt;p&gt;A Context Switch is &lt;strong&gt;not free&lt;/strong&gt;. The act of saving/loading states consumes CPU cycles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the switch to be meaningful, the CPU must spend a minimum amount of time actually processing the logic of the Thread (called &lt;strong&gt;Minimal Execution Time&lt;/strong&gt; ).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Problem:&lt;/strong&gt; When you spawn &lt;em&gt;too many tasks/threads&lt;/em&gt;, the amount of time each thread receives in a cycle becomes smaller. If a thread's execution time shrinks and approaches the time taken just to Context Switch, the CPU falls into a state of &lt;strong&gt;Thrashing&lt;/strong&gt; — meaning the CPU is 100% busy just swapping back and forth between threads, while the actual useful work processed is minimal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FThe-Context-Switch-Paradox-and-Minimal-Execution-Time.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2FThe-Context-Switch-Paradox-and-Minimal-Execution-Time.JPG" alt="How CPU Cores, Processes, and Threads Actually Work" width="789" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2. Real-world Trade-offs
&lt;/h3&gt;

&lt;p&gt;When you spawn too many Threads exceeding the CPU's capacity, the system suffers 3 main consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Increased Latency:&lt;/strong&gt;
The more threads there are, the longer the CPU Run queue. A thread might have to wait a very long time before it's its turn to get a "time slice". This increases the overall latency of the application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU Overhead:&lt;/strong&gt;
As mentioned above, the CPU wastes more resources managing schedules and Context Switching instead of running your business logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Misses - Extremely Severe:&lt;/strong&gt;
The CPU has ultra-fast caches (L1, L2, L3 Cache). When Thread A is running, A's data is loaded into the Cache. If the CPU is forced to Context Switch to Thread B, B's data will push A's data out of the Cache (Cache Pollution). When Thread A comes back to run, the CPU can no longer find the data in the Cache ( &lt;strong&gt;Cache Miss&lt;/strong&gt; ) and must go down to the RAM to read it again — reading from RAM is hundreds of times slower than Cache.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. Real-world Application: Operational Models of Languages/Runtimes
&lt;/h2&gt;

&lt;p&gt;Understanding the costs of Threads and Context Switches, backend technologies have chosen different paths:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. PHP-FPM (Multi-Process / Pre-fork Model)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mechanism:&lt;/strong&gt; Each incoming request is allocated to a &lt;strong&gt;separate Process&lt;/strong&gt; (containing 1 Thread inside).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Characteristics:&lt;/strong&gt; Very safe; an error in one request won't kill another request (due to independent memory spaces).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Massive Performance Drawbacks:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Processes are very heavy (consume a lot of RAM).&lt;/li&gt;
&lt;li&gt;Context Switching between Processes is vastly more expensive than between Threads (because it requires swapping Virtual Memory space / TLB flush).&lt;/li&gt;
&lt;li&gt;If there are 1000 concurrent requests, PHP-FPM needs 1000 Processes. The server will run out of RAM and the CPU will immediately suffer from &lt;em&gt;Thrashing&lt;/em&gt; due to excessive Context Switching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Bottlenecked by the pre-configured number of Worker Processes (e.g., &lt;code&gt;pm.max_children = 50&lt;/code&gt;). Beyond 50 requests, subsequent requests must wait in a queue.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2Fphp-fpm.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2Fphp-fpm.JPG" alt="How CPU Cores, Processes, and Threads Actually Work" width="339" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Node.js (Single Thread &amp;amp; Event Loop Model)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mechanism:&lt;/strong&gt; Node.js realizes that Context Switching is the enemy of performance. Therefore, it runs all JavaScript code on &lt;strong&gt;a single Main Thread&lt;/strong&gt; (1 Process).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Characteristics:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Completely eliminates Context Switch costs for the main logic code. CPU isn't wasted, and Cache rarely Misses.&lt;/li&gt;
&lt;li&gt;Handles tens of thousands of connections (I/O) concurrently thanks to the &lt;strong&gt;Non-blocking I/O&lt;/strong&gt; mechanism (combined with epoll/kqueue at the OS level). When there's a File/Network read task, Node.js offloads it to the background (libuv's Thread pool) and moves on to do other things. Once finished, a callback/event notifies it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2Fnodejs.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2Fnodejs.JPG" alt="How CPU Cores, Processes, and Threads Actually Work" width="652" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drawbacks:&lt;/strong&gt; Because there is only 1 Thread, if you have a heavy computation task (CPU-bound like image compression, a billion-iteration loop), it will &lt;strong&gt;block the entire process&lt;/strong&gt; — other requests will hang waiting until that task completes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Golang (M:N Scheduling &amp;amp; Goroutines Model)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mechanism:&lt;/strong&gt; Go strikes a brilliant balance. It spawns a number of OS Threads (exactly equal to the number of physical CPU Cores to achieve true parallelism). On those OS Threads, it runs tens of thousands, or even millions, of &lt;strong&gt;Goroutines&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Characteristics (Why Go is top-tier?):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ultra-lightweight:&lt;/strong&gt; A Goroutine is a &lt;em&gt;User-space Thread&lt;/em&gt; (a virtual thread managed by the Go Runtime). It only takes about ~2KB of RAM to initialize (compared to 1-2MB for a standard OS Thread).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ultra-cheap Context Switch:&lt;/strong&gt; When Goroutine A is blocked (e.g., calling a DB), the Go Runtime saves A's state and picks Goroutine B to stuff into the OS Thread to continue running. This Context Switch happens at the software level (User-space), &lt;strong&gt;without notifying the Operating System&lt;/strong&gt; , thus the speed is incredibly fast and the cost is a fraction of an OS Context Switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized Cache &amp;amp; CPU:&lt;/strong&gt; By keeping the number of OS Threads low (equal to Cores), Go completely avoids OS-level Context Switch overload and maximizes CPU Cache utilization.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F04%2Fgolang.JPG" alt="How CPU Cores, Processes, and Threads Actually Work" width="717" height="277"&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Summary (The Takeaway)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPUs run Threads, not Processes.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time Slicing&lt;/strong&gt; allows running more threads than cores, but brings latency, CPU overhead, and Cache data loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP&lt;/strong&gt; is safe but heavy because it spawns many Processes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; uses 1 Thread combined with Asynchronous I/O to dodge Context Switches, excelling at I/O but fearing heavy computation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golang&lt;/strong&gt; creates ultra-lightweight "virtual threads" (Goroutines) and schedules them on real Cores itself, maximizing both I/O and computation.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>networking</category>
    </item>
    <item>
      <title>How the Two Most Popular Open-Source Databases (MySQL &amp; PostgreSQL) Handle INSERT, UPDATE, DELETE</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Fri, 20 Mar 2026 09:11:23 +0000</pubDate>
      <link>https://dev.to/quangpv/how-the-two-most-popular-open-source-databases-mysql-postgresql-handle-insert-update-delete-4klf</link>
      <guid>https://dev.to/quangpv/how-the-two-most-popular-open-source-databases-mysql-postgresql-handle-insert-update-delete-4klf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdws386teptmg6hu199e7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdws386teptmg6hu199e7.png" alt="How the Two Most Popular Open-Source Databases (MySQL &amp;amp; PostgreSQL) Handle INSERT, UPDATE, DELETE" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When choosing a relational database for a real-world project, developers often debate surface-level features like JSON support, PostGIS, or clustering capabilities. However, the core performance determinant of a system lies at the lowest tier: how the database organizes physical storage and processes write operations (INSERT, UPDATE, DELETE).&lt;/p&gt;

&lt;p&gt;This article will delve into the underlying architecture of InnoDB (MySQL's default engine) and PostgreSQL. This will help you clearly understand the strengths and weaknesses of each database management system based on fundamental data modification operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Differences in Physical Data Organization
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3awom9woyiv2j35f4wga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3awom9woyiv2j35f4wga.png" alt="How the Two Most Popular Open-Source Databases (MySQL &amp;amp; PostgreSQL) Handle INSERT, UPDATE, DELETE" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before analyzing each operation, we must understand how these two systems arrange data on the hard drive. (Practical Evidence: This difference is exactly why a table containing 1 billion records in PostgreSQL often takes up 10-20% more disk space than MySQL, due to the over-allocation of the Heap buffer).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. MySQL (InnoDB): Clustered Index Storage (B+Tree)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In InnoDB, the table itself is a B+Tree heavily sorted by the Primary Key. The data of a row is stored directly at the leaf nodes of this tree. All Secondary Indexes point to the Primary Key of that row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. PostgreSQL: Heap Storage&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
PostgreSQL uses a free memory region called a Heap to store tuples (rows). Data pushed into the Heap does not need to be sorted by any order or Primary Key. Subsequently, all Indexes (including the Primary and Secondary Indexes) are built on a private B-Tree structure. The leaf nodes of this Index tree contain physical pointers (CTIDs) pointing directly to the exact location of the record in the Heap.&lt;/p&gt;

&lt;p&gt;Understanding this crucial difference will reveal how they behave with I/O.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The INSERT Operation: The Battle of Random IDs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpojuh429wgmejzz4ld4l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpojuh429wgmejzz4ld4l.png" alt="How the Two Most Popular Open-Source Databases (MySQL &amp;amp; PostgreSQL) Handle INSERT, UPDATE, DELETE" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Analyzing the data insertion operation heavily depends on the data type of the Primary Key. (Practical Evidence: Choosing the wrong storage engine when dealing with massive amounts of Random UUIDs can severely degrade the maximum disk write speed from 50,000 IOPS down to just 2,000 IOPS because the disk is locked by the continuous restructuring of tree elements).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Auto-Increment / Sequential IDs:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
MySQL truly shines. Due to the Clustered Index characteristic, sequential data is continuously appended to the extreme right edge of the B+Tree. The write operation is extremely smooth, avoiding disk read head distraction. The physical data sits closely together, giving Range Scan queries exceptionally high speeds.&lt;br&gt;&lt;br&gt;
PostgreSQL also performs this task well; however, it consumes slightly more I/O because it has to write to two different places: inserting data into the empty Heap space, then updating the Index B-Tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Random IDs (UUIDv4):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
PostgreSQL takes absolute priority. The Heap structure is a massive advantage here. It simply finds available memory (Free Space) and randomly inserts data without breaking any memory arrays.&lt;br&gt;&lt;br&gt;
Conversely, this is a nightmare for MySQL. Attempting to insert random, unordered data into a B+Tree means the engine must constantly perform Page Splits to squeeze data in between. This creates severe local locks and skyrockets I/O latency under high load. Therefore, when working with MySQL, engineers are forced to switch to UUIDv7 (time-sequential) or Snowflake IDs instead of UUIDv4.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The UPDATE Operation: The Dark Side of MVCC Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flp3899x32h98iblp2u3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flp3899x32h98iblp2u3t.png" alt="How the Two Most Popular Open-Source Databases (MySQL &amp;amp; PostgreSQL) Handle INSERT, UPDATE, DELETE" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the area that shows the most distinct contrast in the design philosophy of the two systems. (Practical Evidence: Continuously updating driver coordinates every second in Ride-Hailing applications is the classic "Update In-place" problem, where InnoDB's hardware-protecting advantage helps the Database Server avoid suffocating under a mountain of dead data).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL (InnoDB): Update In-Place&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
When the system receives an UPDATE command for a record, if the data size does not bloat larger than the old version, MySQL goes straight to that physical location and confidently overwrites the new data. Naturally, to solve the problem of concurrent readers while writing (MVCC - Multi-Version Concurrency Control), InnoDB uses the Undo Log. The pre-modified version is placed in the Undo Log to serve the read process of older transactions.&lt;br&gt;&lt;br&gt;
Benefit: Only strictly necessary data is modified; the hard drive does not generate unnecessary garbage. If the updated column is not part of any Index, the Index trees remain completely untouched, perfectly preserving hardware resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL: Update is Essentially Delete + Insert New Row&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Unlike MySQL, to ensure the integrity of MVCC, PostgreSQL never overwrites old data. Every time you execute an UPDATE command, PostgreSQL treats it as a logical deletion of the current record and immediately writes the entire information of that row into a completely new physical record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequences:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The first is Write Amplification. Even if you only change the value of one column, Postgres generates a whole new physical row with the exact integrity of the old row.&lt;br&gt;&lt;br&gt;
Next, because the newly generated row resides at a new memory address, PostgreSQL is forced to scan and update ALL the tips of the Index trees pointing to that table to reconnect the physical pointers, regardless of whether the Index pointers were actually changed or not.&lt;/p&gt;

&lt;p&gt;To better understand the hidden price of managing MVCC in PostgreSQL, you can refer to the in-depth blog post: &lt;a href="https://medium.com/@rohanjnr44/internals-of-mvcc-in-postgres-hidden-costs-of-updates-vs-inserts-381eadd35844?ref=blog.quangpv.top" rel="noopener noreferrer"&gt;Internals of MVCC in Postgres: Hidden Costs of updates vs inserts&lt;/a&gt;. The article sharply analyzes why heavily updated systems easily bog down due to Index Thrashing (destroying I/O just to maintain Indexes).&lt;/p&gt;

&lt;p&gt;Although PostgreSQL has a defensive technique (HOT - Heap Only Tuples) by reserving a buffer called fillfactor right inside the same data block, this technique only works if the updated column is not part of any Index structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The DELETE Operation: The Garbage Collection Process
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfxymzjlcdd84zdv0gvd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfxymzjlcdd84zdv0gvd.png" alt="How the Two Most Popular Open-Source Databases (MySQL &amp;amp; PostgreSQL) Handle INSERT, UPDATE, DELETE" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both MySQL and PostgreSQL utilize a Soft-delete model at the system level (marking as deleted instead of immediately sweeping the disk) to save response time, but they differ in their recovery mechanisms. (Practical Evidence: Many startups using PostgreSQL have experienced Database crashes during hot growth phases simply because their autovacuum configuration was too superficial, causing a massive accumulation of "Dead Tuples" that paralyzed the CPU and I/O bandwidth during full table scans).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL (InnoDB):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
At the leaf nodes of the Clustered Index, the deleted record gets a "deleted_flag" bit flipped, and that memory address is attached to a Free List. As soon as another INSERT command of matching size arrives, InnoDB immediately reuses that gap, avoiding wasted space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Similarly, the deleted row transforms into a "Dead Tuple" by attaching the transaction info (txid) of the recent deletion and hiding that row from subsequent arrivals.&lt;br&gt;&lt;br&gt;
Combined with the UPDATE in-place nightmare described above, PostgreSQL spawns an extreme amount of Dead Tuples on the hard drive. The only way to clean up and reclaim this space is to wait for the background garbage collection process (VACUUM Worker) to sweep the entire system. If the data volatility is too high and the autovacuum configuration is not well-tuned, the table will bloat out of control (Table Bloat), forcing the hard drive bandwidth to bear the burden of scanning garbage data before touching real data.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Conclusion via Trade-offs
&lt;/h2&gt;

&lt;p&gt;Choosing a data architecture never involves the concept of an "absolutely superior technology"; rather, the core lies in the trade-offs to solve the right business domain.&lt;/p&gt;

&lt;p&gt;To prove the importance of this architectural trade-off, you can refer to the classic technical analysis: &lt;a href="https://www.uber.com/en-VN/blog/postgres-to-mysql-migration/?ref=blog.quangpv.top" rel="noopener noreferrer"&gt;Why Uber Engineering Switched from Postgres to MySQL&lt;/a&gt;. Even though modern PostgreSQL versions have been significantly upgraded and optimized to reduce the load on the garbage collection process, at the core architectural level, the root MVCC element that causes Write Amplification still fundamentally exists. As long as a record is not updated directly In-place, the system will continue to spend I/O duplicating physical rows.&lt;/p&gt;

&lt;p&gt;In summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your application leans towards static data or one-way appending (Append-only), requires complex analytical queries, polymorphic data structures (JSONB, PostGIS, Array), and the number of records subjected to In-place updates is not disastrous, PostgreSQL remains the undisputed all-in-one powerhouse.&lt;/li&gt;
&lt;li&gt;Conversely, if the core use case of the project is Write-heavy with the frequency of data overwrite (UPDATE) on a finite number of records repeating thousands of times per second, the architecture of MySQL (InnoDB) will protect the server hardware far more perfectly and keep the I/O latency graph absolutely stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hopefully, this deep dive provides you with a clearer understanding of these two prominent databases, empowering you to make informed architectural decisions in real-world applications or ace your next system design interview.&lt;/p&gt;

</description>
      <category>database</category>
    </item>
    <item>
      <title>Understanding File Descriptors: The Silent Resource That Can Crash Your Linux Server</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Sat, 07 Mar 2026 15:25:27 +0000</pubDate>
      <link>https://dev.to/quangpv/understanding-file-descriptors-the-silent-resource-that-can-crash-your-linux-server-2hj1</link>
      <guid>https://dev.to/quangpv/understanding-file-descriptors-the-silent-resource-that-can-crash-your-linux-server-2hj1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhxkuv0q5xvk5uc2xha0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhxkuv0q5xvk5uc2xha0.png" alt="Understanding File Descriptors: The Silent Resource That Can Crash Your Linux Server" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the world of Linux system administration and backend development, we often obsess over CPU usage and RAM consumption. But there is a silent resource that, when exhausted, will bring your entire service to a standstill while your monitoring dashboards still show "plenty of memory": &lt;strong&gt;The File Descriptor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you've ever seen the error &lt;code&gt;EMFILE: too many open files&lt;/code&gt;, you've already met the limit. But what exactly is happening under the hood?&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything is a File
&lt;/h2&gt;

&lt;p&gt;One of the core philosophies of Linux is that &lt;strong&gt;"Everything is a file."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This doesn't just mean your text files or logs. In Linux, the following are all represented as files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network Sockets&lt;/strong&gt; : Every TCP connection, Redis query, or database session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets&lt;/strong&gt; : Every real-time connection from your users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipes&lt;/strong&gt; : Internal communication channels between processes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Listeners&lt;/strong&gt; : Character devices and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;strong&gt;File Descriptor (FD)&lt;/strong&gt; is simply a non-negative integer that the kernel uses as a handle to access these resources. When your program opens a file or a socket, the kernel gives it an FD. When you're done, you're supposed to close it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breaking Point: Resource Limits
&lt;/h2&gt;

&lt;p&gt;Linux systems have strict limits on how many FDs can be open at once—both for the entire system and for each individual process.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;System-wide limits&lt;/strong&gt; : Controlled by the kernel (visible via &lt;code&gt;/proc/sys/fs/file-nr&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process-level limits&lt;/strong&gt; : Often defaults to &lt;code&gt;1024&lt;/code&gt; on many systems (check via &lt;code&gt;ulimit -n&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a process hits its limit, it can no longer open anything new. It can't accept new users, it can't talk to the database, and it can't even write a log entry. The service becomes a "zombie"—alive but completely unresponsive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of an FD Leak
&lt;/h2&gt;

&lt;p&gt;A leak happens when your code opens a resource but fails to close it correctly. Common scenarios include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgotten &lt;code&gt;defer close()&lt;/code&gt;&lt;/strong&gt;: In Go or other languages, skipping the cleanup of HTTP response bodies or file handles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken Connection Pools&lt;/strong&gt; : Database or Redis clients that don't return connections to the pool, creating a new identity with every request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zombie WebSockets&lt;/strong&gt; : Connections that are half-open from the client side but never timed out or closed by the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manual debugging usually involves digging into &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/fd/&lt;/code&gt; or running complex &lt;code&gt;lsof&lt;/code&gt; commands. But in a high-traffic production environment, you need to catch these &lt;em&gt;before&lt;/em&gt; they hit the limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices for Managing File Descriptors
&lt;/h2&gt;

&lt;p&gt;To keep your system healthy, follow these industry best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always close resources&lt;/strong&gt; : Ensure every file, socket, or stream is closed as soon as it is no longer needed. In Go, use &lt;code&gt;defer resource.Close()&lt;/code&gt;; in Java, use &lt;code&gt;try-with-resources&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid unnecessary FDs&lt;/strong&gt; : Don't open files in "write" mode if you only need to read. Use the most restrictive mode possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process in batches&lt;/strong&gt; : If you are handling thousands of files, process them in smaller batches rather than opening them all at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Connection Pools&lt;/strong&gt; : For databases and Redis, always use a connection pool or a framework that manages the lifecycle of FDs for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor proactively&lt;/strong&gt; : Regularly check your program's resource usage to ensure it isn't slowly climbing over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know your limits&lt;/strong&gt; : If your application legitimately needs to handle massive amounts of concurrent traffic (like a high-scale WebSocket server), ensure you increase the &lt;code&gt;ulimit&lt;/code&gt; for that specific process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automating FD Awareness
&lt;/h2&gt;

&lt;p&gt;Detecting these leaks in real-time is the key to stability. Instead of waiting for a crash at 3 AM, you should monitor the &lt;strong&gt;growth rate&lt;/strong&gt; of your FDs. If a process starts with 50 FDs and grows to 500 over an hour without shedding them, you have a leak.&lt;/p&gt;

&lt;p&gt;I've developed a tool called &lt;strong&gt;FD Detector&lt;/strong&gt; to handle this automation. It discovers processes in Docker, Kubernetes, and PM2, analyzes their FD categories (sockets vs. files), and alerts you via Telegram before the threshold is reached.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F03%2Fdemo.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F03%2Fdemo.JPG" alt="Understanding File Descriptors: The Silent Resource That Can Crash Your Linux Server" width="760" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Dive into FD Monitoring
&lt;/h3&gt;

&lt;p&gt;If you want to understand how to track these resources automatically and protect your infrastructure, I've open-sourced the tool and documented the architecture on GitHub.&lt;/p&gt;

&lt;p&gt;The package provides deep insights into your process health, including growth rate tracking and leak category analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learn more about FD monitoring on GitHub&lt;/strong&gt; : &lt;a href="https://github.com/qpv-systems/fd-detector?ref=blog.quangpv.top" rel="noopener noreferrer"&gt;https://github.com/qpv-systems/fd-detector&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Understanding the low-level resources of your OS is the first step toward building truly resilient systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>chores</category>
    </item>
    <item>
      <title>Efficient Transaction Management in NestJS with AsyncLocalStorage</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Mon, 26 Jan 2026 09:16:17 +0000</pubDate>
      <link>https://dev.to/quangpv/efficient-transaction-management-in-nestjs-with-asynclocalstorage-565i</link>
      <guid>https://dev.to/quangpv/efficient-transaction-management-in-nestjs-with-asynclocalstorage-565i</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F01%2Fals_transaction_diagram.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F01%2Fals_transaction_diagram.svg" alt="Efficient Transaction Management in NestJS with AsyncLocalStorage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In complex NestJS applications using TypeORM, managing database transactions across multiple service layers often leads to significant challenges. The main struggle is ensuring data integrity without cluttering your codebase with repetitive transaction logic.&lt;/p&gt;

&lt;p&gt;This post introduces a solution using &lt;strong&gt;AsyncLocalStorage (ALS)&lt;/strong&gt; to transparently manage transaction contexts, effectively resolving common pitfalls.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Source Code&lt;/strong&gt; : &lt;a href="https://github.com/qpv-systems/nest-als-transaction?ref=blog.quangpv.top" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;strong&gt;Package&lt;/strong&gt; : &lt;a href="https://www.npmjs.com/package/nest-als-transaction?ref=blog.quangpv.top" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Developers typically face three major problems when handling transactions:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Data Inconsistency (No Transaction)
&lt;/h3&gt;

&lt;p&gt;By default, TypeORM operations run in their own auto-commit transactions. If a business process involves multiple steps (e.g., creating an order then deducting inventory) and the last step fails, the previous steps are &lt;strong&gt;not rolled back&lt;/strong&gt;. This leaves the database in an inconsistent state, such as an order existing without the corresponding stock deduction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Order created successfully&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Inventory deduction FAILS (e.g., db connection lost)&lt;/span&gt;
  &lt;span class="c1"&gt;// CRITICAL: The order persists, but stock was not deducted!&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inventoryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deductStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Complex Manual Management
&lt;/h3&gt;

&lt;p&gt;To ensure data consistency, developers often resort to manual transaction management. This requires writing repetitive code to handle connections, start transactions, commit changes, and — crucially — handle errors with rollback mechanisms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Repetitive Transaction Code&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateOrderDto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createQueryRunner&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... more operations&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commitTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollbackTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Easy to forget!&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Service Integration Complexity (Prop-Drilling)
&lt;/h3&gt;

&lt;p&gt;This is often the most intrusive issue. When multiple services need to work together within a single atomic operation (e.g., OrderService calls InventoryService), they must share the same transaction instance. This forces you to pass the &lt;code&gt;QueryRunner&lt;/code&gt; object as a parameter to every method in the chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Burden of Passing Parameters&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// inventory.service.ts&lt;/span&gt;
&lt;span class="c1"&gt;// You must add 'queryRunner' to every method signature&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;deductStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;QueryRunner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// You must check if a transaction exists&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;manager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// If you call another internal method, you must pass it again&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Repository Method Updates:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If you use a Base Repository, you must update every method (&lt;code&gt;save&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;) to accept an optional &lt;code&gt;QueryRunner&lt;/code&gt;, making your code verbose and harder to maintain.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Solution: Async Local Storage (ALS)
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;nest-als-transaction&lt;/code&gt; library leverages Node.js &lt;strong&gt;AsyncLocalStorage&lt;/strong&gt; to store the transaction context implicitly during the request lifecycle. This is similar to ThreadLocal in Java, allowing context access anywhere in the execution chain without explicit parameter passing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key Benefits
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Context Propagation&lt;/strong&gt; : The active &lt;code&gt;QueryRunner&lt;/code&gt; is stored globally for the current request. Repositories can retrieve it automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent Nesting&lt;/strong&gt; : If you call &lt;code&gt;executeInTransaction&lt;/code&gt; within an existing transaction, the library automatically creates a &lt;strong&gt;SAVEPOINT&lt;/strong&gt; instead of opening a new connection. This prevents deadlocks and connection pool exhaustion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean Architecture (Zero Prop-Drilling)&lt;/strong&gt;: Services and repositories do not need to know about the transaction context via arguments. They simply focus on business logic.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Integration Guide
&lt;/h2&gt;

&lt;p&gt;To fully leverage this library and remove &lt;code&gt;QueryRunner&lt;/code&gt; parameters from your code, you can integrate &lt;code&gt;TransactionHelper&lt;/code&gt; directly into your Base Repository.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Setup Base Repository
&lt;/h3&gt;

&lt;p&gt;Inject &lt;code&gt;TransactionHelper&lt;/code&gt; and use it to retrieve the active &lt;code&gt;QueryRunner&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/cores/base/base.repository.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DeepPartial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UpdateResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typeorm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TransactionHelper&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nest-als-transaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;baseRepo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Repository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;txHelper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TransactionHelper&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Retrieves the active EntityManager from ALS (if in a transaction)
   * or falls back to the default managers.
   */&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;getManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;txHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQueryRunner&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;manager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DeepPartial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getManager&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Implement other methods utilizing getManager()...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Usage in Services
&lt;/h3&gt;

&lt;p&gt;Once the repository is set up, your services become clean and focused purely on business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/order.service.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;txHelper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TransactionHelper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;orderRepo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;inventoryService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InventoryService&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateOrderDto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Start a transaction scope&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;txHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeInTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

      &lt;span class="c1"&gt;// 1. Create Order&lt;/span&gt;
      &lt;span class="c1"&gt;// The repo automatically picks up the transaction from ALS&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// 2. Call other service&lt;/span&gt;
      &lt;span class="c1"&gt;// No need to pass 'queryRunner' manually.&lt;/span&gt;
      &lt;span class="c1"&gt;// The InventoryService internally uses repositories that also pick up the context.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inventoryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deductStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installation &amp;amp; Setup
&lt;/h2&gt;

&lt;p&gt;Install the library via npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;nest-als-transaction

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import &lt;code&gt;TransactionModule&lt;/code&gt; in your &lt;code&gt;AppModule&lt;/code&gt; (it is global). Ensure you have &lt;code&gt;TypeOrmModule&lt;/code&gt; configured with a &lt;code&gt;DataSource&lt;/code&gt; available for injection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Module&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TypeOrmModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/typeorm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TransactionModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nest-als-transaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;TypeOrmModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;TransactionModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage Options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A: Fully Integrated (Recommended)
&lt;/h3&gt;

&lt;p&gt;Use the Base Repository integration shown above. This keeps your code cleanest by hiding the transaction logic inside the repository layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Manual Access
&lt;/h3&gt;

&lt;p&gt;If you do not wish to modify your Base Repository, you can still access the &lt;code&gt;QueryRunner&lt;/code&gt; manually inside the transaction block. This is useful for incremental adoption.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;txHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeInTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Pass 'qr' manually to legacy code that requires it&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;legacyService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using AsyncLocalStorage for transaction management in NestJS eliminates the noise of manual transaction handling. It provides a robust, clean, and scalable way to ensure data consistency across complex service interactions.&lt;/p&gt;

</description>
      <category>chores</category>
      <category>typescript</category>
    </item>
    <item>
      <title>PostgreSQL Safety Lock: Double-Confirm Protection</title>
      <dc:creator>Harry Phung</dc:creator>
      <pubDate>Fri, 23 Jan 2026 07:47:13 +0000</pubDate>
      <link>https://dev.to/quangpv/postgresql-safety-lock-double-confirm-protection-jdn</link>
      <guid>https://dev.to/quangpv/postgresql-safety-lock-double-confirm-protection-jdn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F01%2Fbanner.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.quangpv.top%2Fcontent%2Fimages%2F2026%2F01%2Fbanner.svg" alt="PostgreSQL Safety Lock: Double-Confirm Protection" width="1200" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It starts like any other day in the development environment. A developer in your team is debugging a tricky issue, SQL client open. They write a quick &lt;code&gt;DELETE FROM orders WHERE id = 123&lt;/code&gt; to clean up some test data.&lt;/p&gt;

&lt;p&gt;They highlight the query. They hit &lt;strong&gt;Execute&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But their hands were too fast, and they missed the &lt;code&gt;WHERE&lt;/code&gt; clause. :]]]&lt;/p&gt;

&lt;p&gt;Suddenly, &lt;code&gt;DELETE 15,000 rows&lt;/code&gt; flashes on the screen.&lt;/p&gt;

&lt;p&gt;In production, strict permissions and backups often save us. But in shared environments like &lt;strong&gt;Dev, Staging, or UAT&lt;/strong&gt; , access is often more open to facilitate speed. A simple "muscle memory" slip by one person can inadvertently wipe out a colleague's test data or corrupt the state of a feature being demoed to a client.&lt;/p&gt;

&lt;p&gt;We needed a way to protect the team from these moments of haste without slowing down our workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Solution: A Database Safety Lock
&lt;/h2&gt;

&lt;p&gt;I built a lightweight, pure-SQL solution for PostgreSQL that introduces a &lt;strong&gt;Double-Confirmation&lt;/strong&gt; step for destructive operations.&lt;/p&gt;

&lt;p&gt;It works like a safety switch on a power tool. If you try to run an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;First Attempt:&lt;/strong&gt; 🛑 &lt;strong&gt;BLOCKED&lt;/strong&gt;. The database triggers an exception:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Second Attempt:&lt;/strong&gt; ✅ &lt;strong&gt;SUCCESS&lt;/strong&gt;. If you immediately re-run the exact same query (same SQL, same session), the lock unlocks and the query executes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This simple friction point forces you to pause and think: &lt;em&gt;"Wait, did I mean to run that?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fqpv-systems%2Fpg-safety-lock%2Fmain%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fqpv-systems%2Fpg-safety-lock%2Fmain%2Fdemo.gif" alt="PostgreSQL Safety Lock: Double-Confirm Protection" width="8" height="5"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. How It Works (Under the Hood)
&lt;/h2&gt;

&lt;p&gt;This isn't a client-side plugin. It's a robust &lt;strong&gt;Server-Side Trigger&lt;/strong&gt; (&lt;code&gt;BEFORE UPDATE OR DELETE&lt;/code&gt;) that runs right inside PostgreSQL.&lt;/p&gt;

&lt;p&gt;Key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Dependencies:&lt;/strong&gt; Just plain PL/pgSQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session-Aware:&lt;/strong&gt; It hashes your query (&lt;code&gt;text&lt;/code&gt;) combined with your Session ID (&lt;code&gt;pid&lt;/code&gt;). You can only confirm &lt;em&gt;your own&lt;/em&gt; queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Bypass:&lt;/strong&gt; Backend services (like your API server or background workers) are automatically whitelisted via &lt;code&gt;application_name&lt;/code&gt;, so automated tests and app logic aren't blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency Safe:&lt;/strong&gt; Multiple developers can use the system continuously without blocking each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Get the Code
&lt;/h2&gt;

&lt;p&gt;The full source code, installation guide, and technical details are available on my GitHub. It's a single SQL script you can drop into any non-production database.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/qpv-systems/pg-safety-lock?ref=blog.quangpv.top" rel="noopener noreferrer"&gt;Get PostgreSQL Safety Lock on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Give it a try and let me know if it saves your dev data (or your sanity)!&lt;/p&gt;

</description>
      <category>database</category>
      <category>chores</category>
    </item>
  </channel>
</rss>
