typescript 119 lines · 4 tabs

Optimistic Locking in NestJS with a TypeORM Version Column and Conflict Exception Filter

Shared by codesnips Aug 2026
4 tabs
import {
  Column,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
  VersionColumn,
} from 'typeorm';

@Entity('documents')
export class Document {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'text' })
  title: string;

  @Column({ type: 'text' })
  body: string;

  // Managed entirely by TypeORM; incremented on every successful save.
  @VersionColumn()
  version: number;

  @UpdateDateColumn()
  updatedAt: Date;
}
4 files · typescript Explain with highlit

Optimistic locking lets concurrent writers proceed without holding a database lock: instead of blocking, each writer reads a row, remembers its version, and only commits if the version has not changed. TypeORM implements this with a @VersionColumn, which it increments on every save and checks in the WHERE clause of the UPDATE. If two requests read the same row and both try to save, the second UPDATE matches zero rows and TypeORM raises an OptimisticLockVersionMismatchError. This snippet wires that mechanism end to end in NestJS so the failure surfaces as a clean HTTP 409 rather than a 500.

In document.entity.ts, the Document entity declares @VersionColumn() version: number. This is the linchpin: the column is entirely managed by TypeORM, so application code never sets it manually. The client is expected to echo back the version it last saw, which is how the server detects stale writes.

In documents.service.ts, update loads the entity, then calls repo.save inside a manager.transaction while passing the caller's expectedVersion. The explicit assignment entity.version = expectedVersion matters — it tells TypeORM which version to guard against, so the generated statement becomes UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?. When the row was already bumped by a concurrent writer, that WHERE fails and TypeORM throws. The service deliberately does not catch the error; it lets it bubble up so the filter can translate it uniformly.

In optimistic-lock.filter.ts, an @Catch(OptimisticLockVersionMismatchError) filter converts the TypeORM error into a ConflictException-shaped 409 response with a stable error code, so clients can distinguish a retryable conflict from a genuine server fault. Returning 409 also nudges well-behaved clients to re-fetch and retry.

In documents.controller.ts, the PATCH handler binds the filter with @UseFilters and pulls the version from an If-Match header, mirroring HTTP's own concurrency primitive. This approach trades a small amount of client complexity for lock-free throughput and is ideal for low-contention resources; under heavy write contention, pessimistic locking or a retry loop may be preferable, since here conflicts are pushed back to the caller.


Related snips

Share this code

Here's the card — post it anywhere.

Optimistic Locking in NestJS with a TypeORM Version Column and Conflict Exception Filter — share card
Link copied