🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)

🔧 Programmierung 🕛 kürzlich 7 Min Lesezeit
0

The Missing Row: Auto-Provisioning Derived Records Without the Race Condition

↗ Quelle (dev.to)
🗣️ Stimme:

Why some records should be created by your system, not your users, and how to do it safely in .NET.



A support ticket lands on your desk: "The Teams page is empty. I added a member, but no team shows up."



You check the API. It's behaving exactly as written:




CODE
  { "items": [], "totalCount": 0 }






Nothing is broken. And that's the problem. The system is faithfully returning nothing, because the row that the page reads from was never created. Somewhere in your design,


you assumed a human would create it first.



This article is about a small, recurring design decision that quietly causes empty dashboards, confused users, and "is this a bug?" tickets: who is responsible for creating

derived records
the user, or the system? and how to let the system do it without introducing duplicate rows or race conditions.



The problem



Let's use a fictional product: a collaboration tool called Loop.



In Loop, the important entities are:




  • An Organization (a paying customer).

  • A Member (a person invited into an organization under a plan).

  • A Team a grouping that members belong to, keyed by (OrganizationId, PlanCode).



The admin dashboard lists Teams. Each team card shows a member count.



Here's the catch in the original design: creating a Member wrote a member row. Creating a Team was a separate, manual step an admin was expected to do first. If an


admin invited members without first creating the matching team, the dashboard showed nothing even though the members clearly existed.



From the user's point of view, they did everything right. From the system's point of view, a required row simply didn't exist.



Why it matters



The Team record isn't independent information. It is fully derivable from the first member invited under a plan. When one entity's existence is implied by another, forcing a

human to create it manually is a design smell. It leads to:





  • Empty states that look like outages. Users can't tell "no data" from "misconfigured."


  • Support load. Every skipped step becomes a ticket.


  • Silent data drift. Members exist with no grouping to roll them up.



The fix is to let the system provision the derived record at the moment it first becomes necessary the first write, instead of relying on a prerequisite step.



The naive solution (and its two traps)



The obvious move: when a member is created, create the team if it isn't there yet.




CODE
  public async Task AddMemberAsync(NewMember input, CancellationToken ct)
{
var member = Member.Create(input);
await _members.AddAsync(member, ct);

// Create the team if this is the first member on this plan.
var exists = await _teams.AnyAsync(
t => t.OrganizationId == input.OrganizationId
&& t.PlanCode == input.PlanCode, ct);

if (!exists)
{
var team = new Team(input.OrganizationId, input.PlanCode);
await _teams.AddAsync(team, ct);
}

await _unitOfWork.SaveChangesAsync(ct);
}






This works in a demo. In production it has two traps.



Trap 1: the check-then-insert race



AnyAsync(...) and AddAsync(...) are not atomic. If two members are invited to the same new team at the same moment, both requests can observe exists == false, and both


insert a team. Now the dashboard shows two identical team cards, and every downstream count is split across them.



An application-level AnyAsync check can never close this window on its own. The only reliable guard is a unique constraint, so the database, the one component that sees

all writes, rejects the second insert.




CODE
  // EF Core model configuration
modelBuilder.Entity<Team>()
.HasIndex(t => new { t.OrganizationId, t.PlanCode })
.IsUnique();






With the constraint in place, turn the "check" into a "try, and treat a uniqueness violation as success", because a violation means someone else already created exactly the


row you wanted:




CODE
  private async Task EnsureTeamExistsAsync(
Guid organizationId, string planCode, CancellationToken ct)
{
var exists = await _teams.AnyAsync(
t => t.OrganizationId == organizationId
&& t.PlanCode == planCode, ct);
if (exists) return;

_teams.Add(new Team(organizationId, planCode));

try
{
await _unitOfWork.SaveChangesAsync(ct);
}
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
{
// A concurrent request created the same team first.
// That is the desired end state, so this is a no-op, not an error.
}
}






The application-level AnyAsync still earns its keep: it avoids a failed insert on the common path. The constraint is what makes the code correct under concurrency. You


want both, the check for efficiency, the constraint for truth.



###Trap 2: the normalization mismatch



The team and the member are joined on PlanCode. If one side stores "pro" and the other stores "pro " (trailing space) or "PRO", the join silently returns nothing.


You'll create a team and members, and the dashboard still shows a count of zero a bug that looks identical to the original one.



Normalize the key in exactly one place, and make both sides use it:




CODE
  public Team(Guid organizationId, string planCode)
{
OrganizationId = organizationId;
// Trim once, here, so the stored key matches how members store it.
PlanCode = planCode.Trim();
Id = Guid.NewGuid();
}






Whatever rule you choose, trim, lowercase, collapse, apply it on every path that reads or writes the key. A join is only as reliable as the normalization behind it.



The retroactive gap



Here's the part teams forget. This fix runs at write time. It fixes every member invited after you ship it.



It does nothing for data that already exists. Organizations that invited members before the change still have no team row. They will look broken until either:




  1. someone invites one more member (the new code then provisions the missing team), or

  2. you run a backfill for the existing data.



Don't skip the backfill and hope the write path heals everything. Write a one-time job that creates the missing rows from what's already there:




CODE
  -- Create the missing team for every distinct (org, plan) that has members
-- but no team yet. Idempotent: safe to run more than once.
INSERT INTO teams (id, organization_id, plan_code, created_at_utc)
SELECT gen_random_uuid(), m.organization_id, TRIM(m.plan_code), now()
FROM members m
LEFT JOIN teams t
ON t.organization_id = m.organization_id
AND t.plan_code = TRIM(m.plan_code)
WHERE t.id IS NULL
GROUP BY m.organization_id, TRIM(m.plan_code);






A forward fix plus a backfill is one complete change, not two optional ones. Ship them together.



Best practices





  • Let the system create what it can derive. If record B's existence is implied by record A, don't make a human create B first.


  • Guard uniqueness at the database, not just in code. Application checks are an optimization; constraints are the guarantee.


  • Make "already exists" a success, not an exception. Idempotent provisioning should converge on the same state no matter how many callers race.


  • Normalize join keys once and everywhere. Most "the data is there but the count is zero" bugs are a normalization mismatch.


  • Pair every write-time fix with a backfill. The forward path won't repair history.



Common mistakes




  • Relying on AnyAsync / SELECT ... IF NOT EXISTS alone under concurrency.

  • Adding the unique index but not handling the violation — turning a race into a 500 for the second caller.

  • Backfilling but forgetting to normalize in the backfill query, so it re-inserts near-duplicates.

  • Provisioning the derived row in a separate transaction from the triggering write, so a partial failure leaves one without the other.



Lessons learned



An empty response is not always "no data." Sometimes it's "the row you're reading from was never anyone's job to create." The most durable fix is to move that responsibility


from the user to the system, and then make the system's version safe under concurrency and honest about history.



The interesting engineering isn't the get-or-create. It's everything around it: the constraint that makes it correct, the normalization that makes the join real, and the


backfill that makes it true for data that already exists.



LinkedIn Account :

Credit: Graphics sourced from Medium

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
3 Quellen
GPT-6 Astra Release Today? OpenAI’s Next Major AI Model Is Almost Here
1 Quelle
Apple accuses OpenAI of destroying evidence as trade-secrets fight intensifies
1 Quelle
Major AI platforms go down in unprecedented simultaneous outage
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten The Missing Row: Auto-Provisioning Derived Records Without the Race Condition

Thematisch verwandte Begriffe: Missing, AutoProvisioning, Derived, Records · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...