<?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: Majid Khazaei</title>
    <description>The latest articles on DEV Community by Majid Khazaei (@majid_khazaei_dev).</description>
    <link>https://dev.to/majid_khazaei_dev</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%2F4123140%2F2c89dc38-0157-46aa-85ba-3416348e1047.png</url>
      <title>DEV Community: Majid Khazaei</title>
      <link>https://dev.to/majid_khazaei_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/majid_khazaei_dev"/>
    <language>en</language>
    <item>
      <title>The Second Race Condition: When Updating a Record Is Just as Dangerous as Creating One</title>
      <dc:creator>Majid Khazaei</dc:creator>
      <pubDate>Wed, 23 Sep 2026 15:17:11 +0000</pubDate>
      <link>https://dev.to/majid_khazaei_dev/the-second-race-condition-when-updating-a-record-is-just-as-dangerous-as-creating-one-4bco</link>
      <guid>https://dev.to/majid_khazaei_dev/the-second-race-condition-when-updating-a-record-is-just-as-dangerous-as-creating-one-4bco</guid>
      <description>&lt;p&gt;&lt;em&gt;A Django concurrency story about &lt;code&gt;read-then-mutate&lt;/code&gt;, silent no-ops, and why one fix is never enough.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 1: The Bug That Didn't Announce Itself
&lt;/h2&gt;

&lt;p&gt;After I fixed the &lt;code&gt;add_member&lt;/code&gt; race condition, I did what most developers do after a win: I closed the ticket and moved on.&lt;/p&gt;

&lt;p&gt;Then a small voice asked a very inconvenient question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If &lt;code&gt;add_member&lt;/code&gt; had a race condition, what about &lt;code&gt;change_member_role&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question turned into an audit. And the audit turned into two more bugs.&lt;/p&gt;

&lt;p&gt;Here's the thing about race conditions: &lt;strong&gt;they don't travel alone&lt;/strong&gt;. When you find one &lt;code&gt;check-then-act&lt;/code&gt; pattern, you're not looking at an isolated mistake — you're looking at a &lt;em&gt;category&lt;/em&gt;. The same developer (me) wrote similar code in similar places, and the same class of bug is hiding in all of them.&lt;/p&gt;

&lt;p&gt;I pulled up every mutation in my &lt;code&gt;WorkspaceService&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;WorkspaceService
├── create                → atomic ✅
├── add_member            → fixed in previous article ✅
├── change_member_role    → ? ← here
├── remove_member         → ? ← and here
├── transfer_ownership    → select_for_update ✅
└── delete                → atomic ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two methods stood out immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_member_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ← no lock!
&lt;/span&gt;        &lt;span class="c1"&gt;# ...
&lt;/span&gt;        &lt;span class="n"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;
        &lt;span class="n"&gt;membership&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="n"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;membership&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;remove_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ← no lock!
&lt;/span&gt;        &lt;span class="c1"&gt;# ...
&lt;/span&gt;        &lt;span class="n"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No lock. No &lt;code&gt;select_for_update&lt;/code&gt;. Just a plain &lt;code&gt;get()&lt;/code&gt;, a mutation, and a save.&lt;/p&gt;

&lt;p&gt;I had a suspicion. But suspicion isn't proof. So I wrote tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 2: The Contracts That Came Before the Tests
&lt;/h2&gt;

&lt;p&gt;Before I could write a test, I had to decide what &lt;em&gt;correct behavior&lt;/em&gt; even meant. This is where the second bug started teaching me things the first one hadn't.&lt;/p&gt;

&lt;p&gt;There's a subtle trap in concurrency testing: if you don't define the contract precisely, you'll end up asserting on things that are inherently non-deterministic. So I wrote three contracts — one strict, one loose, one clever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contract 1: Same-Role Concurrent Change
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;B = MEMBER

Thread 1 (admin_a): B → ADMIN
Thread 2 (admin_c): B → ADMIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Both operations succeed (the second is a no-op)
✅ Exactly one membership
✅ Final role = ADMIN
✅ Exactly one WORKSPACE_ROLE_CHANGED notification
✅ No IntegrityError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: this contract is &lt;em&gt;different&lt;/em&gt; from &lt;code&gt;add_member&lt;/code&gt;'s contract.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;add_member&lt;/code&gt;, a duplicate is an &lt;strong&gt;error&lt;/strong&gt; (400). For &lt;code&gt;change_member_role&lt;/code&gt;, the same role is a &lt;strong&gt;no-op&lt;/strong&gt; (200).&lt;/p&gt;

&lt;p&gt;Why? Because the semantics are different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add_member(B) again
    → B really doesn't want to join twice
    → conflict → 400

change_member_role(B, ADMIN) again
    → B is already ADMIN
    → desired state achieved
    → no-op → 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction is not cosmetic. It changes what you assert on, and — as we'll see — it changes how you catch the bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contract 2: Different-Role Concurrent Change
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread 1: B (MEMBER) → ADMIN
Thread 2: B (MEMBER) → MEMBER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️ Winner is non-deterministic
✅ Exactly one membership
✅ Final role ∈ {MEMBER, ADMIN}
✅ No IntegrityError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here I couldn't assert on the final role — transaction ordering isn't deterministic. This is an &lt;strong&gt;exploration test&lt;/strong&gt;, not a strict contract. It exists to catch invariant violations, not to enforce a specific outcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contract 3: Role Change + Remove
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread 1: B → ADMIN
Thread 2: remove(B)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Final count = 0 (in either ordering)
✅ No IntegrityError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why count = 0?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If role change commits first → B is ADMIN → owner can still remove an admin → count = 0&lt;/li&gt;
&lt;li&gt;If remove commits first → role change hits &lt;code&gt;DoesNotExist&lt;/code&gt; → count = 0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both orderings converge. That's the mark of a good concurrency test — it holds regardless of which thread wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 3: The First RED — A Duplicate Notification
&lt;/h2&gt;

&lt;p&gt;I wrote the same-thread setup I'd used before: two real threads, a &lt;code&gt;threading.Barrier(2)&lt;/code&gt;, real transactions, and &lt;code&gt;connection.close()&lt;/code&gt; in a &lt;code&gt;finally&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.django_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_concurrent_same_role_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_a&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;WorkspaceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change_member_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MembershipRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;admin_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_c&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;WorkspaceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change_member_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MembershipRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;admin_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;successes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_run_concurrent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;change_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;change_c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;successes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="n"&gt;memberships&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&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;assert&lt;/span&gt; &lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MembershipRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN&lt;/span&gt;

    &lt;span class="n"&gt;role_notifs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WORKSPACE_ROLE_CHANGED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;role_notifs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It failed. But not on the assertion I expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED
AssertionError: Expected exactly 1 role-change notification, got 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The membership count was correct. The final role was correct. The database hadn't corrupted anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What had gone wrong was invisible to the database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread A                    Thread B
   │                           │
   ├─ get() → MEMBER           │
   │  (no lock)                ├─ get() → MEMBER
   │                           │  (no lock)
   ├─ role = ADMIN             │
   ├─ save()                   ├─ role = ADMIN
   ├─ NotificationService      ├─ save()
   │  .create()                │
   │  → notif #1               ├─ NotificationService
   │                           │  .create()
   ├─ commit                   │  → notif #2
   │                           │
   │                           ├─ commit
   └──                         └──
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both threads read &lt;code&gt;MEMBER&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; either one committed. Both saw a real transition. Both wrote a notification.&lt;/p&gt;

&lt;p&gt;From the database's perspective, everything is fine. From the &lt;em&gt;user's&lt;/em&gt; perspective, they just got two notifications for one role change.&lt;/p&gt;

&lt;p&gt;This is the same class of bug as &lt;code&gt;check-then-create&lt;/code&gt;, but wearing a different mask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Anti-pattern #1 (previous article):
&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Anti-pattern #2 (this article):
&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different shape. Same underlying flaw: &lt;strong&gt;a window between decision and action that isn't protected against concurrent access.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 4: The Fix — One Line
&lt;/h2&gt;

&lt;p&gt;The right pattern already existed in the codebase. &lt;code&gt;transfer_ownership&lt;/code&gt; was using it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;actor_membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;new_owner_membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the fix was a single line per method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&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;Why does this work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;select_for_update&lt;/code&gt; in PostgreSQL takes a &lt;strong&gt;row-level exclusive lock&lt;/strong&gt;. While Thread A holds it, Thread B waits. When Thread A commits and releases the lock, PostgreSQL hands Thread B the &lt;strong&gt;new version of the row&lt;/strong&gt; — not a stale snapshot.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread A                    Thread B
   │                           │
   ├─ SELECT FOR UPDATE        │
   │  → lock acquired          │
   │  → MEMBER read            ├─ SELECT FOR UPDATE
   │                           │  → waiting for lock
   ├─ role = ADMIN             │
   ├─ save()                   │
   ├─ NotificationService      │
   │  .create() → notif #1     │
   ├─ commit                   │
   │  → lock released          │
   │                           ├─ lock acquired
   │                           ├─ re-read → ADMIN
   │                           ├─ role == ADMIN
   │                           │  → return (no-op)
   │                           ├─ commit
   └──                         └──
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check &lt;code&gt;if membership.role == role: return membership&lt;/code&gt; was already in the code — it had just never been able to do its job because both threads were reading stale data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One line. One fix. One bug gone.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 5: The Second Bug — A Silent No-Op
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;remove_member&lt;/code&gt; method had the same problem. But it had an additional twist that I hadn't anticipated.&lt;/p&gt;

&lt;p&gt;Consider this scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread A (change): read(B) → B=MEMBER
Thread B (remove): read(B) → delete(B) → commit
Thread A: B.role = ADMIN → save()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What does &lt;code&gt;save()&lt;/code&gt; do when the row no longer exists?&lt;/p&gt;

&lt;p&gt;The answer surprised me: &lt;strong&gt;nothing&lt;/strong&gt;. Django sends an &lt;code&gt;UPDATE ... WHERE id = ...&lt;/code&gt;, and the database matches zero rows. &lt;strong&gt;No error is raised.&lt;/strong&gt; Silent no-op.&lt;/p&gt;

&lt;p&gt;But the code continues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;
&lt;span class="n"&gt;membership&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="c1"&gt;# silent no-op
&lt;/span&gt;&lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;      &lt;span class="c1"&gt;# ❌ notification created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: a &lt;code&gt;WORKSPACE_ROLE_CHANGED&lt;/code&gt; notification for a role change that &lt;strong&gt;never actually happened&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is worse than the duplicate-notification bug. In the duplicate case, at least &lt;em&gt;something&lt;/em&gt; real happened. Here, the entire event is a ghost — a notification with no corresponding state change.&lt;/p&gt;

&lt;p&gt;The fix was the same: add &lt;code&gt;select_for_update&lt;/code&gt;. Now Thread A waits for Thread B to commit, and then hits &lt;code&gt;DoesNotExist&lt;/code&gt; — a loud, visible failure instead of a silent one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&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;Two methods. Two bugs. Two lines.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 6: The Third Test — A Test Almost Broke
&lt;/h2&gt;

&lt;p&gt;The contract for role-change + remove looked simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread 1: B → ADMIN
Thread 2: remove(B)
Count should be 0 in either ordering.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it almost wasn't order-independent. Here's what I got wrong first:&lt;/p&gt;

&lt;p&gt;I initially wrote the test using an &lt;strong&gt;admin&lt;/strong&gt; to perform the removal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering 1:
  A: B → ADMIN
  C: remove(B) as ADMIN
  → "Admin cannot remove another admin"
  → count = 1 ❌

Ordering 2:
  C: remove(B) as ADMIN
  → ok
  A: B → DoesNotExist
  → count = 0 ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's order-dependent — the test would pass or fail depending on which thread won. Useless.&lt;/p&gt;

&lt;p&gt;The fix was to change the actor to the &lt;strong&gt;owner&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering 1:
  A: B → ADMIN
  Owner: remove(B)
  → owner can remove admin → count = 0 ✅

Ordering 2:
  Owner: remove(B)
  → ok
  A: B → DoesNotExist → count = 0 ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the test is order-independent. It holds no matter which thread commits first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: in concurrency tests, &lt;strong&gt;choose roles that make the test order-independent&lt;/strong&gt;. If you can't, you're not testing an invariant — you're testing a coin flip.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 7: One More Consideration — Deadlock Potential
&lt;/h2&gt;

&lt;p&gt;Once I decided to add &lt;code&gt;select_for_update&lt;/code&gt;, a new question surfaced: &lt;strong&gt;can this create deadlocks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deadlocks happen when two transactions try to lock the same set of rows in different orders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread A: lock(X) → lock(Y) → ...
Thread B: lock(Y) → lock(X) → ...   ← deadlock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I checked my &lt;code&gt;remove_member&lt;/code&gt; implementation carefully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# target: locked
&lt;/span&gt;&lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;span class="c1"&gt;# actor: NOT locked (plain read)
&lt;/span&gt;&lt;span class="n"&gt;actor_membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;strong&gt;one&lt;/strong&gt; row gets locked — the target membership. The actor's membership is read without a lock.&lt;/p&gt;

&lt;p&gt;That means no thread can ever hold two locks in a conflicting order. &lt;strong&gt;Deadlock is impossible by construction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Had I locked the actor as well — which is a natural thing to want to do — I would have opened the door to deadlocks in multi-step operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The principle&lt;/strong&gt;: lock the minimum set of rows required, and lock them in a consistent order. More locks ≠ safer. More locks = more chances to deadlock.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 8: Five Lessons From the Second Bug
&lt;/h2&gt;

&lt;p&gt;This second round of bug-hunting taught me things the first one couldn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;check-then-act&lt;/code&gt; isn't the only race-prone pattern
&lt;/h3&gt;

&lt;p&gt;The first article was about &lt;code&gt;check-then-create&lt;/code&gt;. This one is about &lt;code&gt;read-then-mutate&lt;/code&gt;. The shapes are different, but the underlying flaw is the same:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Any operation with a read followed by a write, without a lock, is race-prone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix isn't "add another check." The fix is either a lock or an atomic database operation. There is no third option.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Silent no-ops are more dangerous than loud failures
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;save()&lt;/code&gt; hits a deleted row, Django doesn't raise. It sends an &lt;code&gt;UPDATE&lt;/code&gt; matching zero rows and moves on.&lt;/p&gt;

&lt;p&gt;If your code continues past that point — sending a notification, updating a counter, logging an event — you've now built a system that &lt;strong&gt;creates side effects for events that never happened&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The only way to catch this class of bug is a concurrency test with realistic ordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. In concurrency tests, choose roles that make the test order-independent
&lt;/h3&gt;

&lt;p&gt;If a test passes in one ordering and fails in another, it's not testing an invariant — it's testing the scheduler. Rewrite the test so the assertion holds regardless of which thread wins. If you can't, the contract isn't fully specified yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Lock only what you need — and always in the same order
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;select_for_update&lt;/code&gt; on every row you touch seems safe. It isn't. It increases the chance of deadlocks and reduces concurrency for no benefit.&lt;/p&gt;

&lt;p&gt;Lock the rows you &lt;em&gt;actually&lt;/em&gt; mutate. Read the rest without locking. When you must lock multiple rows, lock them in a consistent order across the codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A consistent pattern is worth more than a clever fix
&lt;/h3&gt;

&lt;p&gt;After this chapter, every membership mutation follows the same rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WorkspaceService
├── add_member            → Nested Atomic + Error Conversion
├── change_member_role    → select_for_update
├── remove_member         → select_for_update
├── transfer_ownership    → select_for_update (already)
└── delete                → atomic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new developer reading this code sees a pattern. A code reviewer spots deviations in seconds. A future bug surfaces faster because the inconsistency stands out.&lt;/p&gt;

&lt;p&gt;Consistency isn't glamorous, but it's what makes a codebase survivable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 9: The Results
&lt;/h2&gt;

&lt;p&gt;After two fixes, the full test suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_concurrent_same_role_change        ✅ PASSED
test_concurrent_different_role_change   ✅ PASSED
test_concurrent_role_change_and_remove  ✅ PASSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three tests. Two bugs. Two lines of implementation code.&lt;/p&gt;

&lt;p&gt;The commits were kept separate, because tests and fixes are different stories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Commit 1: Tests&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"test(workspace): cover concurrent role change scenarios"&lt;/span&gt;

&lt;span class="c"&gt;# Commit 2: Implementation&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"fix(workspace): serialize concurrent membership mutations with row locks"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a future developer bisects these commits, they'll see: &lt;em&gt;first the test that proves the bug, then the fix that closes it.&lt;/em&gt; That's how you write a changelog that teaches.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The first race condition taught me that &lt;code&gt;check-then-create&lt;/code&gt; is dangerous.&lt;/p&gt;

&lt;p&gt;The second one taught me something harder: &lt;strong&gt;race conditions are a category, not an incident.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you find one, don't patch it and move on. Audit the sibling methods. Look for the same shape under a different name. Assume that if you made the mistake once, you made it — or something close to it — everywhere you wrote similar code.&lt;/p&gt;

&lt;p&gt;The two bugs in this chapter were hiding in plain sight. They passed every unit test. They passed every integration test. They passed code review. They looked correct.&lt;/p&gt;

&lt;p&gt;And under load — under &lt;em&gt;real&lt;/em&gt; load, in &lt;em&gt;real&lt;/em&gt; production, with &lt;em&gt;real&lt;/em&gt; users hitting the same endpoint from different devices — they would have quietly corrupted the state of the system, one duplicate notification at a time.&lt;/p&gt;

&lt;p&gt;The fix was two lines.&lt;/p&gt;

&lt;p&gt;The lessons were worth much more.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Majid Khazaei is a backend engineer specializing in Django, PostgreSQL, and production-grade API design. He writes about concurrency, data integrity, and the engineering discipline behind reliable systems.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/majidkhazaei" rel="noopener noreferrer"&gt;https://github.com/majidkhazaei&lt;/a&gt;&lt;br&gt;
🔗 &lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/majid-khazaei-dev" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/majid-khazaei-dev&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;code&gt;#django&lt;/code&gt;, &lt;code&gt;#python&lt;/code&gt;, &lt;code&gt;#concurrency&lt;/code&gt;, &lt;code&gt;#backend&lt;/code&gt;, &lt;code&gt;#postgresql&lt;/code&gt;, &lt;code&gt;#testing&lt;/code&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>django</category>
      <category>python</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The Concurrency Bug That Only Appears Under Load: A Django Race Condition Story</title>
      <dc:creator>Majid Khazaei</dc:creator>
      <pubDate>Sat, 19 Sep 2026 14:44:53 +0000</pubDate>
      <link>https://dev.to/majid_khazaei_dev/the-concurrency-bug-that-only-appears-under-load-a-django-race-condition-story-14n0</link>
      <guid>https://dev.to/majid_khazaei_dev/the-concurrency-bug-that-only-appears-under-load-a-django-race-condition-story-14n0</guid>
      <description>&lt;h2&gt;
  
  
  Section 1: The Bug That Never Shows Up in Development
&lt;/h2&gt;

&lt;p&gt;There's a category of bugs that haunts every backend engineer: the ones that pass every test you write, work perfectly on your laptop, and then — one day — silently break in production.&lt;/p&gt;

&lt;p&gt;I found one of those bugs last week. Not in a library, not in a framework. In my own code.&lt;/p&gt;

&lt;p&gt;It started with a simple feature: a user adds another user to a workspace. The service layer looked clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is already a member of workspace &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MembershipRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;notification_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WORKSPACE_JOINED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&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="n"&gt;membership&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it once. Read it twice. It looks correct. There's a check, there's an insert, there's a notification. All inside an atomic block. What could possibly go wrong?&lt;/p&gt;

&lt;p&gt;That pattern — &lt;strong&gt;check-then-create&lt;/strong&gt; — is one of the most natural things a developer can write. And it has a flaw that only reveals itself when two requests arrive at the &lt;em&gt;exact same time&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I wrote a concurrency test to prove the code was safe. Instead, the test proved it wasn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 2: The Journey — From a Green Test to a Hidden 500
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Writing the test that would expose everything
&lt;/h3&gt;

&lt;p&gt;To test concurrency properly, I couldn't just call the service twice in a row. Sequential calls aren't concurrency — they're just two isolated operations. I needed two real requests racing against each other.&lt;/p&gt;

&lt;p&gt;That meant three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Real transactions&lt;/strong&gt; — not the wrapped ones pytest uses by default&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two independent threads&lt;/strong&gt; — each with its own database connection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A synchronization barrier&lt;/strong&gt; — so both threads start their critical section at the same moment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The setup looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.django_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_concurrent_add_member_creates_one_membership_and_one_notification&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WorkspaceFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;actor_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;actor_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nc"&gt;MembershipFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MembershipRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OWNER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;MembershipFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MembershipRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;barrier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Barrier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;successes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor&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="n"&gt;barrier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# ← both threads stop here until both arrive
&lt;/span&gt;            &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WorkspaceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;successes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&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="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor_a&lt;/span&gt;&lt;span class="p"&gt;,)),&lt;/span&gt;
        &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor_b&lt;/span&gt;&lt;span class="p"&gt;,)),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;successes&lt;/span&gt;&lt;span class="p"&gt;)&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;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&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;assert&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The barrier is the key. Without it, one thread always finishes before the other even starts, and you're not testing concurrency — you're testing race conditions that don't exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: When green isn't good enough
&lt;/h3&gt;

&lt;p&gt;The test passed. Two threads, one success, one failure. The final state was correct: exactly one membership, exactly one notification.&lt;/p&gt;

&lt;p&gt;But something felt off. I added a diagnostic line to inspect the error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IntegrityError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not &lt;code&gt;ValueError&lt;/code&gt;. &lt;strong&gt;&lt;code&gt;IntegrityError&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For those unfamiliar with Django: &lt;code&gt;ValueError&lt;/code&gt; is a domain error. You raise it intentionally. It's what a ViewSet catches and converts into a clean &lt;code&gt;400 Bad Request&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IntegrityError&lt;/code&gt; is a database error. It means the SQL layer rejected your query. Left uncaught, it becomes a raw &lt;code&gt;500 Internal Server Error&lt;/code&gt; — the kind of error that pages you at 3 AM and makes users think your product is broken.&lt;/p&gt;

&lt;p&gt;The race condition wasn't in the &lt;em&gt;data&lt;/em&gt; — the database's &lt;code&gt;UNIQUE&lt;/code&gt; constraint had protected the data perfectly. The race was in the &lt;strong&gt;error semantics&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Understanding the actual race
&lt;/h3&gt;

&lt;p&gt;I traced through the timeline, and it became obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread 1: exists()?  → NO
Thread 2: exists()?  → NO       ← both threads pass the check
Thread 1: create()   → ✅ commit
Thread 2: create()   → ❌ IntegrityError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both threads run inside &lt;code&gt;transaction.atomic()&lt;/code&gt;. But PostgreSQL's default isolation level is &lt;strong&gt;READ COMMITTED&lt;/strong&gt;. That means each transaction can only see data that has already been &lt;em&gt;committed&lt;/em&gt; by other transactions.&lt;/p&gt;

&lt;p&gt;Neither thread has committed when the other runs &lt;code&gt;exists()&lt;/code&gt;. So both see "no existing membership" and both proceed to insert.&lt;/p&gt;

&lt;p&gt;One wins. One loses. And the loser surfaces a raw database error to the user.&lt;/p&gt;

&lt;p&gt;This is &lt;em&gt;the&lt;/em&gt; textbook example of why check-then-create is dangerous. And it's exactly the kind of bug that never shows up in development — because nobody clicks "add member" twice in the same millisecond on their laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Choosing between three fixes
&lt;/h3&gt;

&lt;p&gt;I considered three approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: &lt;code&gt;get_or_create&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Replace the check-then-create with Django's &lt;code&gt;get_or_create&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_or_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MembershipRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMBER&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already a member&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks elegant. It isn't. &lt;code&gt;get_or_create&lt;/code&gt; is &lt;em&gt;itself&lt;/em&gt; check-then-create underneath — it just hides the pattern behind a helper. Under the same race, both threads can fail the &lt;code&gt;get&lt;/code&gt;, both attempt the &lt;code&gt;create&lt;/code&gt;, and one still hits &lt;code&gt;IntegrityError&lt;/code&gt;. Same bug, more subtly disguised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B: &lt;code&gt;select_for_update&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Lock the workspace row before checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Workspace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# then check-then-create
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates the race entirely — but at a cost. Every &lt;code&gt;add_member&lt;/code&gt; on the same workspace now serializes behind a database lock. Under real load, that's a throughput bottleneck. Two users adding members to the same workspace would queue behind each other, even though there was never any actual conflict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option C: Catch &lt;code&gt;IntegrityError&lt;/code&gt;, convert to &lt;code&gt;ValueError&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Let the database do its job, then translate its error into a domain error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MembershipRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;IntegrityError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is already a member of workspace &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the option I chose. Here's why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: The nested atomic — and why it matters
&lt;/h3&gt;

&lt;p&gt;The natural first attempt is to catch &lt;code&gt;IntegrityError&lt;/code&gt; at the outer level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;   &lt;span class="c1"&gt;# ← IntegrityError happens here
&lt;/span&gt;    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;IntegrityError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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;This doesn't work.&lt;/strong&gt; Django detects that an error occurred inside an atomic block, marks the transaction as broken, and raises &lt;code&gt;TransactionManagementError&lt;/code&gt; on the &lt;em&gt;next&lt;/em&gt; query. You can catch the &lt;code&gt;IntegrityError&lt;/code&gt;, but you can't use the database connection afterward.&lt;/p&gt;

&lt;p&gt;The fix is to wrap only the insert in a &lt;strong&gt;nested atomic block&lt;/strong&gt; — a savepoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;                    &lt;span class="c1"&gt;# outer transaction
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;            &lt;span class="c1"&gt;# ← savepoint
&lt;/span&gt;            &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;IntegrityError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;

    &lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;           &lt;span class="c1"&gt;# outer transaction still healthy
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;membership&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nested &lt;code&gt;atomic()&lt;/code&gt; creates a &lt;code&gt;SAVEPOINT&lt;/code&gt; in PostgreSQL. When the &lt;code&gt;IntegrityError&lt;/code&gt; fires, only the savepoint rolls back. The outer transaction survives and remains usable.&lt;/p&gt;

&lt;p&gt;I placed the savepoint &lt;strong&gt;only around the insert&lt;/strong&gt;, not around the notification. Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the notification ever raised &lt;code&gt;IntegrityError&lt;/code&gt; (a bug, not a race), converting it to "already a member" would be misleading.&lt;/li&gt;
&lt;li&gt;The savepoint should be as small as possible — the tighter the scope, the more precise the semantics.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 6: The exception chaining detail
&lt;/h3&gt;

&lt;p&gt;One subtle line matters more than it looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;from exc&lt;/code&gt; preserves the exception chain. In production logs, the developer sees both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: User user2 is already a member of workspace workspace-0
    ↑ caused by
IntegrityError: duplicate key value violates unique constraint "unique_membership"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user gets a clean &lt;code&gt;400 Bad Request&lt;/code&gt; with a meaningful message. The developer gets a full stack trace explaining exactly what happened. Both win.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Proving the fix at the API level
&lt;/h3&gt;

&lt;p&gt;A service-level fix isn't enough. I also needed to prove that the HTTP contract held under concurrency. Two simultaneous &lt;code&gt;POST&lt;/code&gt; requests to the members endpoint should produce exactly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One &lt;code&gt;201 Created&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One &lt;code&gt;400 Bad Request&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero &lt;code&gt;500 Internal Server Error&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.django_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_concurrent_add_member_returns_201_and_400_not_500&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# ... same threading setup ...
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&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="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;force_authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;barrier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="n"&gt;status_codes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# ... start and join threads ...
&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_codes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected {{201, 400}}, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status_codes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;set()&lt;/code&gt; comparison. During development, I initially wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_codes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And got this beautifully confusing error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssertionError: Expected [201, 400], got [201, 400]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sorted()&lt;/code&gt; sorts &lt;em&gt;ascending&lt;/em&gt;, so &lt;code&gt;sorted([201, 400])&lt;/code&gt; is &lt;code&gt;[201, 400]&lt;/code&gt;, not &lt;code&gt;[400, 201]&lt;/code&gt;. Order-independent assertions need &lt;code&gt;set&lt;/code&gt;, not &lt;code&gt;sorted&lt;/code&gt;. Small mistake, big lesson: &lt;strong&gt;in concurrency tests, never assume order&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The result
&lt;/h3&gt;

&lt;p&gt;Before the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service test: ❌ Expected ValueError, got IntegrityError
API test:     ❌ Expected [201, 400], got [201, 500]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service test: ✅ exactly one ValueError, no IntegrityError
API test:     ✅ exactly {201, 400}, never 500
full suite:   ✅ all green
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full architecture came out cleaner too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB Layer      → unique constraint: data integrity protected
Service Layer → IntegrityError → ValueError: domain semantics enforced
API Layer     → ValueError → 400: HTTP contract preserved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No layer is overstepping its role. The view doesn't need to know about database constraints. The service doesn't need to know about HTTP status codes. Each layer converts errors into the language of the layer above it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 3: Five Lessons I Took Away
&lt;/h2&gt;

&lt;p&gt;Debugging a race condition in a single service method taught me more about backend architecture than any framework tutorial ever could. Here are the five lessons that stuck.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. If your code uses check-then-create, you have a race condition
&lt;/h3&gt;

&lt;p&gt;Every single time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is &lt;em&gt;everywhere&lt;/em&gt; in real codebases — and it's &lt;em&gt;always&lt;/em&gt; wrong under concurrency. The window between &lt;code&gt;check&lt;/code&gt; and &lt;code&gt;act&lt;/code&gt; is small, but it's real, and under load it will be hit.&lt;/p&gt;

&lt;p&gt;The fix isn't a smarter check. The fix is either a database constraint, a lock, or a caught exception. But it's never "just add another check."&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The database constraint is not the bug — the error handling is
&lt;/h3&gt;

&lt;p&gt;I initially thought the database had let me down because it threw an &lt;code&gt;IntegrityError&lt;/code&gt; under load. It hadn't. The database was doing its job perfectly — protecting data integrity under concurrent access.&lt;/p&gt;

&lt;p&gt;The bug was &lt;strong&gt;in my service layer&lt;/strong&gt;: I hadn't accounted for the fact that the constraint could fire. The database is a safeguard, not a substitute for correct error handling.&lt;/p&gt;

&lt;p&gt;Once I framed it that way, the fix became obvious: convert the database's error into the language of the domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Savepoints are not optional — they're the mechanism
&lt;/h3&gt;

&lt;p&gt;When I first tried to catch &lt;code&gt;IntegrityError&lt;/code&gt; inside a transaction, Django raised &lt;code&gt;TransactionManagementError&lt;/code&gt;. I didn't understand why until I read the docs carefully.&lt;/p&gt;

&lt;p&gt;The rule: &lt;strong&gt;once a transaction is broken, it's broken.&lt;/strong&gt; You can't "undo" the break. But you &lt;em&gt;can&lt;/em&gt; isolate the broken operation inside a savepoint so that the outer transaction never sees the damage.&lt;/p&gt;

&lt;p&gt;This is what nested &lt;code&gt;transaction.atomic()&lt;/code&gt; does. It's not a syntax trick — it's the way Django exposes PostgreSQL's savepoint semantics. Any error recovery inside a transaction requires it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The right fix has zero cost in the normal path
&lt;/h3&gt;

&lt;p&gt;Of the three solutions I considered, &lt;code&gt;select_for_update&lt;/code&gt; was the most obviously safe. Lock the workspace, serialize the requests, done.&lt;/p&gt;

&lt;p&gt;But it would serialize &lt;em&gt;every&lt;/em&gt; &lt;code&gt;add_member&lt;/code&gt; call, even the ones that never raced. In the common case — one user adding one member, no concurrency — I'd be paying for a lock I didn't need.&lt;/p&gt;

&lt;p&gt;The exception-conversion approach has zero overhead in the normal path. &lt;code&gt;Membership.objects.create()&lt;/code&gt; runs, succeeds, and no exception handler executes. Only in the rare race does the code take the &lt;code&gt;except&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best fix is often the one that does nothing in the common case.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Concurrency tests are the ones you'll never see fail — until production does
&lt;/h3&gt;

&lt;p&gt;Every test in this project passed before I wrote the concurrency tests. Every single one. Unit tests, integration tests, API tests — green across the board.&lt;/p&gt;

&lt;p&gt;They were green because they were testing the code the way developers write it: one call, one result. The race condition only exists when &lt;em&gt;two&lt;/em&gt; calls happen at &lt;em&gt;the same time&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If I hadn't written a test with &lt;code&gt;threading.Barrier&lt;/code&gt; and &lt;code&gt;transaction=True&lt;/code&gt;, this bug would have shipped. And it would have been invisible for months. And then one day, in production, with real users on real networks hitting real servers, some poor engineer would have seen a spike in 500s and had no idea where to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If a bug can only exist under concurrency, only a concurrency test can find it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The pattern that caused this bug — &lt;code&gt;if not exists(): create()&lt;/code&gt; — is the first thing most developers reach for. It's intuitive. It reads like English. It passes code review because it &lt;em&gt;looks&lt;/em&gt; correct.&lt;/p&gt;

&lt;p&gt;But "looks correct" and "is correct under load" are different things. The gap between them is where production bugs live.&lt;/p&gt;

&lt;p&gt;What changed my approach wasn't learning a new library or memorizing a design pattern. It was accepting that &lt;strong&gt;any code touching shared state can be raced&lt;/strong&gt; — and being willing to write the tests that prove it can't.&lt;/p&gt;

&lt;p&gt;The fix itself was four lines. The savepoint around one &lt;code&gt;.create()&lt;/code&gt;. The conversion from &lt;code&gt;IntegrityError&lt;/code&gt; to &lt;code&gt;ValueError&lt;/code&gt;. The preservation of the exception chain.&lt;/p&gt;

&lt;p&gt;But those four lines are the difference between a &lt;code&gt;400 Bad Request&lt;/code&gt; and a &lt;code&gt;500 Internal Server Error&lt;/code&gt;. Between a system that degrades gracefully under load and one that collapses.&lt;/p&gt;

&lt;p&gt;The next time you write &lt;code&gt;if not exists()&lt;/code&gt;, pause. Ask yourself what happens if two users hit that line at the same time. If the answer is "I don't know" — that's the test to write.&lt;/p&gt;

&lt;p&gt;Not every race condition has a race at its heart. But every one has a lesson.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About the author:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Majid Khazaei is a backend engineer specializing in Django, PostgreSQL, and production-grade API design. He writes about concurrency, data integrity, and the engineering discipline behind reliable systems.&lt;/p&gt;

&lt;p&gt;🔗 GitHub: &lt;a href="https://github.com/majidkhazaei" rel="noopener noreferrer"&gt;https://github.com/majidkhazaei&lt;/a&gt;&lt;br&gt;
🔗 LinkedIn: &lt;a href="https://www.linkedin.com/in/majid-khazaei-dev" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/majid-khazaei-dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>debugging</category>
      <category>django</category>
      <category>python</category>
    </item>
    <item>
      <title>My Journey Contributing to Django REST Framework</title>
      <dc:creator>Majid Khazaei</dc:creator>
      <pubDate>Sun, 13 Sep 2026 12:27:16 +0000</pubDate>
      <link>https://dev.to/majid_khazaei_dev/my-journey-contributing-to-django-rest-framework-3lo1</link>
      <guid>https://dev.to/majid_khazaei_dev/my-journey-contributing-to-django-rest-framework-3lo1</guid>
      <description>&lt;h2&gt;
  
  
  Section 1: The Bug That Sat Untouched for Over a Year
&lt;/h2&gt;




&lt;p&gt;In September 2026, I merged my first Pull Request into Django REST Framework — one of the most widely used Python libraries in the world.&lt;/p&gt;

&lt;p&gt;But the story didn't start with me. It started with a bug that was reported in &lt;strong&gt;May 2025&lt;/strong&gt; and sat untouched for over a year.&lt;/p&gt;

&lt;p&gt;The bug was subtle but serious: DRF's serializer validation was &lt;strong&gt;rejecting perfectly valid data&lt;/strong&gt; when using Django's &lt;code&gt;UniqueConstraint&lt;/code&gt; with conditions.&lt;/p&gt;

&lt;p&gt;Here's what that means in practice. Imagine you're building an API for a race tracking system. You want to enforce a simple rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Only one race with a given name can have a position ≤ 1."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So you write this constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;UniqueConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;race_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position__lte&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="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unique_top_race_name&lt;/span&gt;&lt;span class="sh"&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 database handles this correctly. It allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;Marathon, position=1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;Marathon, position=2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;Marathon, position=3&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But DRF's serializer? It would &lt;strong&gt;incorrectly reject&lt;/strong&gt; the third entry with a "unique set" error — even though the database would happily accept it.&lt;/p&gt;

&lt;p&gt;The user experience was broken. Developers were forced to bypass the serializer and insert data directly into the database, which is exactly the kind of thing DRF exists to prevent.&lt;/p&gt;

&lt;p&gt;The worst part? The issue was marked as &lt;strong&gt;"confusing"&lt;/strong&gt; by one of the maintainers themselves. It involved complex interactions between &lt;code&gt;Q&lt;/code&gt; objects, &lt;code&gt;referenced_base_fields&lt;/code&gt;, &lt;code&gt;nulls_distinct&lt;/code&gt;, and two competing validator classes.&lt;/p&gt;

&lt;p&gt;Nobody wanted to touch it.&lt;/p&gt;

&lt;p&gt;That's exactly why I did.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 2: The Journey — Reviving an Abandoned PR, Surviving Three Merge Conflicts, and Facing Copilot
&lt;/h2&gt;




&lt;p&gt;Once I decided to tackle this bug, I discovered that someone had already started working on it — a developer named &lt;code&gt;nefrob&lt;/code&gt; had opened a Pull Request months earlier, but it had stalled.&lt;/p&gt;

&lt;p&gt;The PR had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Merge conflicts with the main branch&lt;/li&gt;
&lt;li&gt;❌ Missing helper functions it depended on&lt;/li&gt;
&lt;li&gt;❌ Unresolved reviewer feedback&lt;/li&gt;
&lt;li&gt;❌ No activity for weeks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most people would have walked away. Instead, I decided to &lt;strong&gt;revive it&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I actually did
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Rebased and resolved the initial conflicts.&lt;/strong&gt;&lt;br&gt;
I pulled the abandoned branch, resolved the merge conflicts, and added a missing helper function (&lt;code&gt;get_referenced_base_fields_from_q&lt;/code&gt;) that the original author had referenced but never committed. Without it, the whole PR wouldn't even import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Faced three rounds of new merge conflicts.&lt;/strong&gt;&lt;br&gt;
Every time a maintainer merged something new into &lt;code&gt;main&lt;/code&gt;, my branch would break again. I resolved conflicts &lt;strong&gt;three separate times&lt;/strong&gt; — each one requiring me to carefully combine my logic with changes from other contributors who were working on the same file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Fixed four bugs found by GitHub Copilot.&lt;/strong&gt;&lt;br&gt;
Copilot reviewed my PR and found issues I had missed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Empty &lt;code&gt;constraint.fields&lt;/code&gt;&lt;/strong&gt; — my code was creating validators with no fields, silently breaking validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lost custom error messages&lt;/strong&gt; — when multiple constraints shared the same fields, only the last one's message survived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition fields not triggering revalidation&lt;/strong&gt; — updates that only changed the condition field were silently accepted, even when they violated the database constraint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;nulls_distinct&lt;/code&gt; edge case&lt;/strong&gt; — partial updates were incorrectly skipping validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each bug required careful investigation. Each one needed a test to prove the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Got reviewed by a contributor who'd worked on the same code.&lt;/strong&gt;&lt;br&gt;
A contributor named &lt;code&gt;MehrazRumman&lt;/code&gt; — who had recently merged his own PR touching the same file — reviewed my work in detail. He found two more issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;dead-code helper&lt;/strong&gt; that could be removed entirely (since DRF only supports Django 5.2+, which already provides &lt;code&gt;Q.referenced_base_fields&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;performance regression&lt;/strong&gt; where partial updates triggered three unnecessary database queries instead of zero.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I applied both fixes. That meant deleting 20 lines of code I'd originally added — and that was the right call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Passed the final review and got merged.&lt;/strong&gt;&lt;br&gt;
After 17 commits, 26 comments, and more than a month of back-and-forth, the PR was approved and merged by a DRF maintainer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The result
&lt;/h3&gt;

&lt;p&gt;The final change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Fixes incorrect validation for conditional &lt;code&gt;UniqueConstraint&lt;/code&gt;s&lt;/li&gt;
&lt;li&gt;✅ Preserves custom error messages and error codes&lt;/li&gt;
&lt;li&gt;✅ Correctly rechecks uniqueness when condition fields change&lt;/li&gt;
&lt;li&gt;✅ Handles &lt;code&gt;nulls_distinct&lt;/code&gt; edge cases&lt;/li&gt;
&lt;li&gt;✅ Passes all 83 validator tests&lt;/li&gt;
&lt;li&gt;✅ Merged into Django REST Framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A bug that had been open for over a year — and that one maintainer openly called "confusing" — was finally closed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 3: Five Lessons I Took Away (And Why They Matter Beyond Open Source)
&lt;/h2&gt;




&lt;p&gt;Merging a PR into Django REST Framework taught me more than any tutorial could. Here are the five lessons that stuck with me — and that apply far beyond open source.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Reviving abandoned work is more valuable than starting from scratch
&lt;/h3&gt;

&lt;p&gt;The original PR had been sitting untouched for months. Instead of writing my own solution from zero, I picked up where someone else left off — resolved the conflicts, filled in the gaps, and carried it across the finish line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; In any team, the unfinished work of others is often the highest-leverage place to contribute. Don't ignore the graveyard of "almost done" projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Automated tools find what humans miss — but they don't replace judgment
&lt;/h3&gt;

&lt;p&gt;GitHub Copilot found four bugs in my code that I had completely overlooked. Each one was subtle, each one was real, and each one would have caused problems in production.&lt;/p&gt;

&lt;p&gt;But Copilot also made suggestions I chose &lt;strong&gt;not&lt;/strong&gt; to follow — and I had to justify why. Automation is a force multiplier, not a decision-maker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Use AI tools aggressively to review your work, but always own the final call.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The best code is often less code
&lt;/h3&gt;

&lt;p&gt;A reviewer pointed out that one of my helper functions was &lt;strong&gt;dead code&lt;/strong&gt; — a compatibility shim for Django versions the project no longer supported. I deleted 20 lines I'd been proud of writing.&lt;/p&gt;

&lt;p&gt;That deletion was the single best thing I did in the entire PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Code you don't write is code you don't have to maintain, test, or debug. Simplicity is a feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Merge conflicts are a sign of a healthy project — not a broken one
&lt;/h3&gt;

&lt;p&gt;I resolved conflicts &lt;strong&gt;three separate times&lt;/strong&gt; during this PR. Every time, it meant other contributors were actively improving the same code I was touching.&lt;/p&gt;

&lt;p&gt;A project with zero merge conflicts is usually a project with zero activity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Friction from collaboration is not a bug. It's the cost of working on something that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Patience is a technical skill
&lt;/h3&gt;

&lt;p&gt;From my first comment to the final merge, this took over a month. There were days of waiting for reviewers, weeks of silence from maintainers, and moments when I wondered if it was worth continuing.&lt;/p&gt;

&lt;p&gt;It was. The maintainers are volunteers. The reviewers have day jobs. And the bug wasn't urgent — it had waited a year already. It could wait one more week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; In open source — and in any long-running project — patience isn't passive. It's an active skill you have to practice.&lt;/p&gt;




&lt;h3&gt;
  
  
  Final thought
&lt;/h3&gt;

&lt;p&gt;I didn't contribute to Django REST Framework because I'm an expert. I did it because I was willing to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the issue carefully&lt;/li&gt;
&lt;li&gt;Pick up someone else's unfinished work&lt;/li&gt;
&lt;li&gt;Accept feedback without taking it personally&lt;/li&gt;
&lt;li&gt;Delete my own code when it wasn't needed&lt;/li&gt;
&lt;li&gt;Wait when waiting was the right move&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A year-old bug that one maintainer called "confusing" is now closed. And a small piece of my work is running in thousands of production APIs around the world.&lt;/p&gt;

&lt;p&gt;If you've been waiting for the "right moment" to contribute to open source — this is it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick an issue. Start small. Be patient. Ship it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  About the author:
&lt;/h2&gt;

&lt;p&gt;Majid Khazaei is a software engineer and open-source contributor. &lt;br&gt;
He recently contributed to Django REST Framework, fixing a validation bug &lt;br&gt;
that had been open for over a year (PR #10021).&lt;/p&gt;

&lt;p&gt;🔗 GitHub: &lt;a href="https://github.com/majidkhazaei" rel="noopener noreferrer"&gt;https://github.com/majidkhazaei&lt;/a&gt;&lt;br&gt;
🔗 LinkedIn: &lt;a href="https://www.linkedin.com/in/majid-khazaei-dev" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/majid-khazaei-dev&lt;/a&gt;&lt;/p&gt;




</description>
      <category>backend</category>
      <category>opensource</category>
      <category>python</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
