Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close
    Lesson1 / 20

    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.

    C#
    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

    Text
    planner 2 -- Charge(2,80) --+
                               +--> join --> sort by Id --> coordinator
    planner 1 -- Charge(1,30) --+                         owns balance
                                                            |
                                                    check + apply

    Identifier 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?

    A Task.Run always processes inputs backwards.
    B The check and update do not form one protected transition.
    C An integer cannot represent a balance.
    D A record automatically serializes access.
    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?

    A Task 1 always completes first.
    B Task.WhenAll sorts by the Id property.
    C OrderBy explicitly defines the coordinator's processing order.
    D The smaller charge always wins.
    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?

    A Previously started planners are automatically killed.
    B The coordinator debits every successfully planned charge.
    C The balance becomes zero.
    D Awaiting the join throws before the debit loop begins.
    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.
    Sign in to mark lessons done and keep your place in the course.Sign in

    TechInterviewNotes Editorial Team · Updated 27 Oct 2026 ·