# Idempotency and Concurrent Requests

Repeated and concurrent order requests must preserve accepted outcomes without duplicate work. Compare canonical request identity, allowed race outcomes, stored facts, and outgoing effects.

Published: 2026-06-15
Updated: 2026-09-27
Source: https://matlus.com/writing/idempotency-and-concurrent-requests/
Tags: acceptance-testing, idempotency, concurrency, data-access, csharp

---
A customer places an order and then submits it again. Perhaps the first response
was lost or the caller retried while waiting. The service needs a defined answer
for repeated requests and for different requests claiming the same order reference.

> **About the examples:** Meridian Ordering is a private training reference project. The inline listings illustrate the techniques; the full C# and Python repositories are not currently public.

Meridian's rule preserves the original outcome for identical content. Different
content under an existing reference is a conflict. Neither case may create another
order or repeat the original outgoing work. These are observable business and
technical obligations, exercised through the same public operation.

## Establish what identical means

Identity belongs to the request's meaning. SKU casing is canonicalized before
comparison, so differently cased versions of the same SKU still describe the
same product. Customer identity, ordered products, and quantities determine whether
the request agrees with the recorded order.

The complete edge-case class
contains identical and conflicting resubmissions. The identical scenario:

1. Arranges a customer, products, request, and expected recorded lines.
2. Opens its broker subscription before the original placement.
3. Places the original order and consumes its notification.
4. Submits equivalent content with changed SKU casing.
5. Compares the response with the original response, reads the stored order,
   checks one order and unchanged lines, then checks for extra broker or email work.

Consuming the first notification is important. Otherwise a delayed original
message could be mistaken for work produced by the resubmission.

Here is the complete identical-resubmission test:

<!-- Listing source: tests/MeridianOrdering.Tests/Acceptance/DomainFacade/TestPlaceOrderEdgeCases.cs; method: PlaceOrder_WhenIdenticalRequestIsResubmittedWithSameReference_ThenOriginalOutcomeIsReturnedWithoutDuplicates -->

```csharp
/// Arrange, Act, Assert: compare a repeated request with the original accepted outcome.
public async Task PlaceOrder_WhenIdenticalRequestIsResubmittedWithSameReference_ThenOriginalOutcomeIsReturnedWithoutDuplicates()
{
    /// Arrange - Given an order already placed, over two catalog products read from the store
    var customerRecord = ArrangedCustomers.CreateArrangedCustomer(_expectedOrderPlacementIdentifiers.CustomerId);
    int firstLineQuantity = 3;
    int secondLineQuantity = 2;
    // Price-capped so the order is Placed (BR-6) whatever the draw - AC-10 asserts
    // the original Placed outcome is returned unchanged.
    decimal maximumUnitPrice = DataGenerators.GetMaximumUnitPriceWithoutTriggeringReview(AcceptanceTestConstants.HeldForReviewThresholdTotal, firstLineQuantity + secondLineQuantity);
    var catalogProducts = DataManagerForTest.GetRandomCatalogProducts(
        2, maximumUnitPrice: maximumUnitPrice);
    var firstCatalogProduct = catalogProducts[0];
    var secondCatalogProduct = catalogProducts[1];
    var orderPlacementRequest = RequestBuilders.OrderPlacementRequestBuilder
        .Set(request => request.OrderReference, _expectedOrderPlacementIdentifiers.OrderReference)
        .Set(request => request.CustomerId, customerRecord.CustomerId)
        .Set(request => request.OrderPlacementLineItems,
        [
            new OrderPlacementLineItem(sku: firstCatalogProduct.Sku, quantity: firstLineQuantity),
            new OrderPlacementLineItem(sku: secondCatalogProduct.Sku, quantity: secondLineQuantity),
        ])
        .Build();
    var caseVariantResubmission = RequestBuilders.OrderPlacementRequestBuilder
        .Set(request => request.OrderReference, _expectedOrderPlacementIdentifiers.OrderReference)
        .Set(request => request.CustomerId, customerRecord.CustomerId)
        .Set(request => request.OrderPlacementLineItems,
        [
            new OrderPlacementLineItem(sku: LetterCasing.SwapCase(firstCatalogProduct.Sku), quantity: firstLineQuantity),
            new OrderPlacementLineItem(sku: LetterCasing.SwapCase(secondCatalogProduct.Sku), quantity: secondLineQuantity),
        ])
        .Build();
    decimal expectedFirstLineTotal = firstCatalogProduct.UnitPrice * firstLineQuantity;
    decimal expectedSecondLineTotal = secondCatalogProduct.UnitPrice * secondLineQuantity;
    decimal expectedOrderTotal = expectedFirstLineTotal + expectedSecondLineTotal;
    int expectedRecordedOrderCount = 1;
    int expectedEmailRequestCount = 1;
    List<RecordedOrderLine> expectedOrderLines =
    [
        new RecordedOrderLine(
            Sku: firstCatalogProduct.Sku,
            ProductName: firstCatalogProduct.ProductName,
            Quantity: firstLineQuantity,
            UnitPrice: firstCatalogProduct.UnitPrice,
            LineTotal: expectedFirstLineTotal),
        new RecordedOrderLine(
            Sku: secondCatalogProduct.Sku,
            ProductName: secondCatalogProduct.ProductName,
            Quantity: secondLineQuantity,
            UnitPrice: secondCatalogProduct.UnitPrice,
            LineTotal: expectedSecondLineTotal),
    ];
    var (domainFacadeInstance, testMediatorEmailService, _) = DomainFacadeFactory.CreateDomainFacade();
    await using var domainFacade = domainFacadeInstance;
    // Subscribed BEFORE the original placement and its message consumed explicitly,
    // so the quiet window after the resubmission can only ever see a message the
    // resubmission sent - deterministic on both brokers (a durable Service Bus
    // subscription could otherwise surface the original message after an
    // open-time drain, whenever propagation runs slower than the drain).
    await using var subscriberFulfillmentNotifications =
        await BrokerSubscriptions.SubscribeToFulfillmentNotificationsForTestAsync();
    // The original response is the expected outcome: resubmission must return it unchanged.
    var expectedPlacementWindowStartUtc = DateTime.UtcNow;
    OrderPlacementResult expectedOrderPlacementResult = await domainFacade.PlaceOrderAsync(orderPlacementRequest);
    await BrokerSubscriptions.ReceiveMessageForOrderAsync(
        subscriberFulfillmentNotifications, orderPlacementRequest.OrderReference);

    /// Act - When identical content arrives with only SKU casing changed
    OrderPlacementResult actualOrderPlacementResult = await domainFacade.PlaceOrderAsync(caseVariantResubmission);

    /// Assert - the caller receives the original success response
    const string IdempotencyAssertionFailedHeader = "\nIDEMPOTENCY ASSERTION FAILED (BR-7)\n";
    Assert.True(
        actualOrderPlacementResult == expectedOrderPlacementResult,
        $"{IdempotencyAssertionFailedHeader}" +
        $"Expected (original) response:    {expectedOrderPlacementResult}\n" +
        $"Actual (resubmitted) response:   {actualOrderPlacementResult}\n");

    /// Assert - no additional order or order line exists afterward
    int actualRecordedOrderCount = DataManagerForTest.CountRecordedOrders(orderPlacementRequest.OrderReference);
    Assert.True(
        actualRecordedOrderCount == expectedRecordedOrderCount,
        $"{IdempotencyAssertionFailedHeader}" +
        $"Expected exactly {expectedRecordedOrderCount} recorded order for reference " +
        $"'{orderPlacementRequest.OrderReference}' but found {actualRecordedOrderCount}.\n");
    var actualRecordedOrder = DataManagerForTest.GetRecordedOrder(orderPlacementRequest.OrderReference);
    var actualPlacementWindowEndUtc = DateTime.UtcNow;
    AsserterOrderStore.AssertCustomerStatusTotalLinesAndPlacementTimeAreAsExpected(
        expectedCustomerId: customerRecord.CustomerId,
        expectedOrderPlacementStatus: OrderPlacementStatus.Placed,
        expectedOrderTotal: expectedOrderTotal,
        expectedOrderLines: expectedOrderLines,
        expectedPlacementWindowStartUtc: expectedPlacementWindowStartUtc,
        actualPlacementWindowEndUtc: actualPlacementWindowEndUtc,
        actualRecordedOrder: actualRecordedOrder);

    /// Assert - fulfillment receives no additional notification and no additional email is sent
    var actualUnexpectedFulfillmentNotificationPayload = await BrokerSubscriptions.TryReceiveMessageForOrderAsync(
        subscriberFulfillmentNotifications, orderPlacementRequest.OrderReference);
    AsserterMessageBroker.AssertNoMessageArrived(
        actualUnexpectedFulfillmentNotificationPayload,
        orderPlacementRequest.OrderReference,
        "AC-10: fulfillment receives no additional notification");
    int actualEmailRequestCount = testMediatorEmailService.CapturedEmailRequests.Count;
    Assert.True(
        actualEmailRequestCount == expectedEmailRequestCount,
        $"\nIDEMPOTENCY ASSERTION FAILED (BR-7 / AC-10)\n" +
        $"Expected no additional email: the email service should have received exactly " +
        $"{expectedEmailRequestCount} request (the original placement's), but received {actualEmailRequestCount}.\n");
}
```

Using the first response as the expected resubmission response is appropriate
for the claim that the original outcome is preserved. It does not independently
establish that the first placement was correct. The scenario also compares stored
facts with catalog-derived expectations, and the complete successful-placement
suite establishes the original acceptance behavior.

## Refuse conflicting content without changing the original

The conflict scenario places an order, then reuses its reference with different
line content. It expects OrderReferenceConflictException with the required
classification and reference context. It reads the original order afterward and
compares its original customer, status, total, and lines. It checks no additional
notification and no extra email.

An exception alone would leave unanswered whether the original order was altered
before refusal. The complete conflict test
makes preservation part of the outcome.

## Start two requests before collecting their outcomes

> Diagram: Assert the allowed outcome counts and stored content without predicting a winner. These concurrent submissions demonstrate the observed schedules, with bounded checks for extra effects.

Sequential repetition leaves one request fully completed before the next begins.
Concurrent requests can both reach placement while neither caller yet has a result.
The database's unique reference constraint must arbitrate their attempts.

Here is Meridian's complete concurrency helper:

<!-- Listing source: tests/MeridianOrdering.Tests/Acceptance/DomainFacade/TestPlaceOrderTechnicalRequirements.cs; method: RaceBothPlacementsAsync -->

```csharp
/// Act: start both operations before awaiting and collecting their outcomes.
private static async Task<List<object>> RaceBothPlacementsAsync(
    DomainFacade domainFacade,
    OrderPlacementRequest orderPlacementRequestOne,
    OrderPlacementRequest orderPlacementRequestTwo)
{
    Task<OrderPlacementResult> placementTaskOne = domainFacade.PlaceOrderAsync(orderPlacementRequestOne);
    Task<OrderPlacementResult> placementTaskTwo = domainFacade.PlaceOrderAsync(orderPlacementRequestTwo);
    List<object> actualConcurrentPlacementOutcomes = [];
    foreach (Task<OrderPlacementResult> placementTask in new[] { placementTaskOne, placementTaskTwo })
    {
        try
        {
            actualConcurrentPlacementOutcomes.Add(await placementTask);
        }
        catch (MeridianOrderingException meridianOrderingException)
        {
            actualConcurrentPlacementOutcomes.Add(meridianOrderingException);
        }
    }

    return actualConcurrentPlacementOutcomes;
}
```

It invokes both asynchronous operations before awaiting either task. It then
collects successful results or domain exceptions. It does not use Task.WhenAll,
and an unexpected exception escapes and fails the test.

This arrangement permits overlap. It does not force both requests to pause at
the same database statement, nor enumerate every possible schedule. One task
can progress substantially before the other reaches the contested operation.
Describe the evidence as outcomes of these concurrent submissions.

## Compare allowed outcomes without predicting the winner

The differing-content race requires one success, one conflict, and zero unexpected
outcomes. The outcome comparison counts types rather than assuming the first
request must win:

<!-- Listing source: tests/MeridianOrdering.TestSupport/Asserters/AsserterConcurrentOrderPlacement.cs; method: CompareOutcomeCounts -->

```csharp
/// Assert: compare allowed outcome counts without depending on completion order.
public static string? CompareOutcomeCounts(
    int expectedSuccessCount, int expectedConflictCount, int expectedUnexpectedOutcomeCount,
    IReadOnlyList<object> actualPlacementOutcomes)
{
    int actualSuccessCount = actualPlacementOutcomes.OfType<OrderPlacementResult>().Count();
    int actualConflictCount = actualPlacementOutcomes.OfType<OrderReferenceConflictException>().Count();
    int actualUnexpectedOutcomeCount = actualPlacementOutcomes.Count - actualSuccessCount - actualConflictCount;
    List<string> discrepancies = [];
    if (actualSuccessCount != expectedSuccessCount)
    {
        discrepancies.Add($"Success count: expected {expectedSuccessCount}; actual {actualSuccessCount}");
    }

    if (actualConflictCount != expectedConflictCount)
    {
        discrepancies.Add($"Conflict count: expected {expectedConflictCount}; actual {actualConflictCount}");
    }

    if (actualUnexpectedOutcomeCount != expectedUnexpectedOutcomeCount)
    {
        discrepancies.Add($"Unexpected outcome count: expected {expectedUnexpectedOutcomeCount}; actual {actualUnexpectedOutcomeCount}");
    }

    return discrepancies.Count == 0 ? null : "CONCURRENCY ASSERTION FAILED\n" + string.Join("\n", discrepancies) +
        $"\nOutcomes: {string.Join("; ", actualPlacementOutcomes)}";
}
```

The complete concurrent scenarios
also check the resulting store and outbound effects. The identical-content race
expects both callers to receive the same accepted result while only one order
and one set of outgoing actions result. The differing-content scenario identifies
the winning request before comparing stored customer and line content.

The constraint acts in the real database. A separate check that asks whether a
reference exists cannot alone prevent two callers passing that check together.
The schema and placement procedure
show how the actual transaction and uniqueness authority participate.

## Repetition must preserve outstanding work too

A failed original email leaves its action Pending. The resubmission scenario
creates a new facade with a recovered email endpoint and submits the same order.
It verifies that request repetition does not itself replay the action and that
the Pending state remains. The accepted order's original outcome still stands.

This separates request idempotency from recovery policy. An out-of-band recovery
worker, if introduced, needs tests for its own operation, state changes, recipient
deduplication, and ambiguous completion cases.

## Keep concurrency, capacity, and delivery claims precise

The broker quiet window supplies bounded evidence of no additional correlated
notification. Email captures supply counts within the awaited operation. Together
with store checks, they establish the demonstrated request behavior.

They do not establish exactly-once delivery across crashes, later recovery, or
every schedule. Load and resilience testing address additional specified concerns,
such as resource limits under sustained contention. Those responsibilities join
the release plan without weakening the functional suite's purpose.

With requirements established and correct scenarios and expectations covering
these outcomes, green boundary tests give the team confidence that this build
preserves its promised repeated-request behavior.

---

[Previous article](/writing/refusals-failures-and-work-still-owed/) | [Series contents](/acceptance-testing/) | [Next article](/writing/testing-the-service-interface/)