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;
}
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, Repository } from 'typeorm';
import { Document } from './document.entity';
interface UpdateDocumentInput {
title?: string;
body?: string;
}
@Injectable()
export class DocumentsService {
constructor(
@InjectRepository(Document)
private readonly repo: Repository<Document>,
) {}
async update(
id: string,
expectedVersion: number,
input: UpdateDocumentInput,
): Promise<Document> {
return this.repo.manager.transaction(async (manager: EntityManager) => {
const entity = await manager.findOne(Document, { where: { id } });
if (!entity) {
throw new NotFoundException(`Document ${id} not found`);
}
Object.assign(entity, input);
// Pin the version we read so TypeORM guards the UPDATE against it.
entity.version = expectedVersion;
// Throws OptimisticLockVersionMismatchError if a concurrent write won.
return manager.save(entity);
});
}
}
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
import { OptimisticLockVersionMismatchError } from 'typeorm';
@Catch(OptimisticLockVersionMismatchError)
export class OptimisticLockFilter implements ExceptionFilter {
catch(_exception: OptimisticLockVersionMismatchError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.status(HttpStatus.CONFLICT).json({
statusCode: HttpStatus.CONFLICT,
error: 'Conflict',
code: 'STALE_VERSION',
message:
'The resource was modified by another request. Re-fetch and retry.',
});
}
}
import {
BadRequestException,
Body,
Controller,
Headers,
Param,
Patch,
UseFilters,
} from '@nestjs/common';
import { DocumentsService } from './documents.service';
import { OptimisticLockFilter } from './optimistic-lock.filter';
import { UpdateDocumentDto } from './update-document.dto';
@Controller('documents')
export class DocumentsController {
constructor(private readonly documents: DocumentsService) {}
@Patch(':id')
@UseFilters(OptimisticLockFilter)
async update(
@Param('id') id: string,
@Headers('if-match') ifMatch: string,
@Body() dto: UpdateDocumentDto,
) {
const expectedVersion = Number(ifMatch);
if (!ifMatch || Number.isNaN(expectedVersion)) {
throw new BadRequestException('Valid If-Match version header required');
}
return this.documents.update(id, expectedVersion, dto);
}
}
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
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.