Module 1 · Own state and coordinate concurrent work · Lesson 1 of 20
Advanced diagnostic: state the invariant and reason about ownership
Two charges, 30 and 80, compete for a balance of 100. Can a final balance of 20 or 70 prove that the program never approved more money than it had?
If both workers read 100 before either updates it, each can approve its charge. The last write may leave a nonnegative balance even though the program approved 110. The check and its update need one owner.
Decide which charge gets priority
This program considers positive charges in ascending identifier order. A successful debit reduces the balance once; a rejected debit changes nothing. The planners are listed as identifier 2, then 1. Predict the accepted charge and the final balance before running the complete program in a .NET 10 console project.
using System;
using System.Linq;
using System.Threading.Tasks;
Task<Charge>[] planners =
[
Task.Run(() => new Charge(2, 80)),
Task.Run(() => new Charge(1, 30))
];
Charge[] charges = await Task.WhenAll(planners);
int balance = 100;
int accepted = 0;
foreach (Charge charge in charges.OrderBy(c => c.Id))
{
bool applied = TryDebit(ref balance, charge.Amount);
if (applied) accepted++;
Console.WriteLine($"{charge.Id}:{(applied ? "accepted" : "rejected")}");
}
if (balance < 0 || accepted != 1)
throw new InvalidOperationException("Invariant broken.");
Console.WriteLine($"Balance={balance}");
static bool TryDebit(ref int balance, int amount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
if (amount > balance) return false;
balance -= amount;
return true;
}
readonly record struct Charge(int Id, int Amount);Trace the decision, not the task schedule
planner 2 -- Charge(2,80) --+
+--> join --> sort by Id --> coordinator
planner 1 -- Charge(1,30) --+ owns balance
|
check + applyIdentifier 1 requests 30, leaving 70. Identifier 2 then requests 80 and is rejected. The output is 1:accepted, 2:rejected, then Balance=70. OrderBy defines that business order; the order in which the planners finish does not.
The rule that must remain true is the invariant: approvals never exceed the available balance, and a rejection changes nothing. One coordinator owns the balance. Workers return immutable Charge values and never receive the balance or a mutable account object.
ref makes TryDebit's mutation visible to its caller. The single coordinator makes that mutation safe here. Task.WhenAll supplies the earlier join: every planner finishes before the debit loop begins. If a planner fails, the await throws before any charge is applied. A failure after the loop has begun would require a separate partial-work policy.
Both workers check a shared balance before either updates it. Which review finding identifies the core defect?
Show answer
Correct answer: B
An approval must be based on the state that its own transition updates.
Option A: Task scheduling provides no such reverse-order guarantee.
Option C: The example uses whole units and stays within the integer range.
Option D: A record describes a data type; it does not coordinate unrelated storage.
Hint: Identify the decision that depends on current state.
Extend the batch
Add a third planner returning identifier 3 and amount 50. Write the full trace and update the accepted-count assertion before running. Then review the double-approval design from the opening: point to the two statements that must share one mutation boundary.
Pause here. Work out both answers before using the hint.
Hint: a rejected charge leaves 70 available for the next identifier.
Check your result
The extended trace is 1:accepted, 2:rejected, 3:accepted, then Balance=20; the accepted count is 2. Changing only the final-balance assertion cannot repair double approval. The availability check and subtraction must be coordinated together.
Try amount 0 separately. The guard throws before mutation: invalid arguments and a valid but unaffordable charge have different outcomes.
Why is identifier 1 processed first in the program?
Show answer
Correct answer: C
The coordinator sorts returned charge values before applying them.
Option A: The program makes no completion-order assumption.
Option B: WhenAll joins tasks; it does not inspect domain identifiers.
Option D: The sort key is Id, not Amount.
A planner throws before Task.WhenAll completes. What is this program's batch policy?
Show answer
Correct answer: D
The debit loop follows the awaited join, so a planning failure prevents this batch from applying charges.
Option A: WhenAll does not forcibly stop other tasks.
Option B: The code never reaches the debit loop on that failure path.
Option C: No failure handler assigns zero to the balance.
Before continuing: if the planners finish in reverse order, which line still makes identifier 1 receive priority?
References: Task.WhenAll, cooperative cancellation model.
Quick reference
- Write the invariant before selecting a concurrency primitive.
- Keep a decision and its state transition within one ownership boundary.
- Immutable messages do not make other shared objects safe.
- Join every task whose result or lifetime belongs to the operation.
- Define business ordering separately from scheduling.
TechInterviewNotes Editorial Team · Updated 27 Oct 2026 ·