Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring GraphQL mutation versions: reject a stale stock reservation

Last updated: 1 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

A mutation can require an expected version so a second writer cannot silently overwrite a changed receipt.

Download Spring source kit

Make the conflict a data rule

The checked mutation reserves seven units from RC-47 at version 3, leaving 12 units and version 4. A second request carrying expectedVersion 3 produces a GraphQL error and leaves the balance at 12. The in-memory ledger uses ConcurrentHashMap.computeIfPresent to make that one-process read-check-write atomic. A deployed database needs a conditional UPDATE with tenant, ID, version and stock floor in the predicate, followed by an affected-row check. The JDBC lesson tests that database contract separately.

A wrong-tenant mutation returns null and leaves RC-47 unchanged. The test injects tenant identity into GraphQL context itself; no authentication filter validates it. Tenant resolver boundaries explain why that source of identity must change before deployment.

Keep side effects outside a failed write

If the mutation also emits an event, the event intent must be committed with the database change or published from a durable outbox. Sending to a broker before the versioned update succeeds can announce a reservation that never happened. The outbox transaction and broker handoff cover those cut points.

Checked excerpt

Java
Receipt result = ledger.computeIfPresent(id, (ignored, current) -> {
    if (!current.tenant().equals(tenant(environment))) return current;
    if (current.version() != expectedVersion) {
        throw new IllegalStateException("stale receipt version");
    }
    if (current.available() < units) {
        throw new IllegalStateException("insufficient stock");
    }
    return new Receipt(current.id(), current.tenant(),
        current.available() - units, current.version() + 1);
});

Verification boundary

The Java 21 / Spring Boot 4 source kit checks mutationUsesVersionAndDecrementsOnce, staleMutationReturnsErrorWithoutAnotherDebit and wrongTenantMutationDoesNotChangeTarget.

Cost and limits

The local map update serializes writes per key within one process. It has no persistence, cross-node conflict control or database transaction. A conditional SQL update needs one round trip plus indexed row access; contention and retries depend on the target database.

Common Mistakes

  • Do not check the version in one query and update in a later unguarded query.
  • Do not publish an event before confirming the mutation committed.
  • Do not treat a test-supplied tenant context as authenticated identity.

Read next

Spring JDBC versioned tenant update: inspect the affected row count, Spring GraphQL tenant reads: scope every resolver before returning a record, Spring transactional outbox: commit a receipt and event row together, Spring outbox to Kafka: name the crash window between send and relay acknowledgement, Spring GraphQL errors: distinguish validation, resolver failure and nullable data.

Reactive database continuation

Continue with Spring R2DBC conditional update: test tenant, version and stock floor in one statement.

spring
spring-boot
graphql
graphql-optimistic-mutation
Storage details