Logo

dev-resources.site

for different kinds of informations.

Taming cross-service database transactions in NestJS with AsyncLocalStorage

Published at
2/21/2024
Categories
prisma
typeorm
sequelize
nestjs
Author
papooch
Categories
4 categories in total
prisma
open
typeorm
open
sequelize
open
nestjs
open
Author
7 person written this
papooch
open
Taming cross-service database transactions in NestJS with AsyncLocalStorage

The Problem

If you've ever bumped into the issue that you needed a single database transaction to span multiple services without breaking encapsulation of the repository layer, you'll know what I'm talking about.

If you're coming from a framework where this was just a thing (e.g. the @Transactional annotation from Spring Hibernate, @transaction.atomic from Django, or automatic contextual transactions from .NET Entity Framework) and then you learned that there's nothing like that built into NestJS, I feel your frustration.

You are either trying to create an involved solution yourself, awkwardly breaking encapsulation by passing the transaction client as a parameter around your services or straight up abandoning the idea.

This post, should serve as a guide on how to solve this issue using most known ORMs. And yes, you might be bummed to hear that the solution is often to install a 3rd party library, but that's just the world we live in.

By the way, yes, I can hear the DDD evangelists sayin that if you model your aggregates right, you won't ever need cross-service transaction. Well, yes, but not all problems need to be modeled that way.

The Solution

All of the solutions rely in one way or another on a little known, but very powerful, Node.js API called AsyncLocalStorage. If you're not familiar with the concept, I encourage you to give it a read before continuing.

Sequelize

You'll be pleased to know that Sequelize has this feature built-in, all you need is to install cls-hooked (which is a predecessor of AsyncLocalStorage, but will be replaced by it in Sequelize v7) and enable it globally with Sequelize.useCLS(...)

After that, wrapping a service call in a transaction callback would make sure the same transaction is re-used by all Sequelize queries made within the callback even without passing it explicitly.

await this.connection.transaction(async () => {
  return await this.otherService.doWork()
})
Enter fullscreen mode Exit fullscreen mode

TypeORM

TypeORM doesn't have this feature, but a maintained community package called typeorm-transactional exists to solve this shortcoming. It behaves almost exactly like the @Transactional annotation in Spring Hibernate with complete support for custom repositories.

Creating and propagating a transaction is then as simple as decorating a method with @Transactional

@Injectable()
export class PostService {
  constructor(
    private readonly repository: PostRepository,
    private readonly dataSource: DataSource
  ) {}

  @Transactional()
  async createAndGetPost(id, message): Promise<Post> {
    const post = this.repository.create({ id, message })

    await this.repository.save(post)

    return dataSource.createQueryBuilder(Post, 'p').where('id = :id', id).getOne();
  }
}
Enter fullscreen mode Exit fullscreen mode

Prisma

There have been multiple feature requests to add native support for AsyncLocalStorage to Prisma, but they haven't been met with much enthusiasm from the maintainers. Some people solved it by extending and overriding the client (which is arguably prone to breaking with updates).

However, now you can also use @nestjs-cls/transactional (made by yours truly) to enable a similar behavior.

The library also takes inspiration from Spring Hibernate (mainly to ease the learning curve), so the usage is very similar to the former, only with a little bit of extra boilerplate at the expense of delibrately not monkey-patching any underlying library.

@Injectable()
class UserService {
  constructor(
    @InjectTransaction()
    private readonly tx: Transaction<TransactionalAdapterPrisma>,
    private readonly accountService: AccountService,
  ) {}

  @Transactional()
  async createUser(name: string): Promise<User> {
    const user = await this.tx.user.create({ data: { name } });
    await this.accountService.createAccountForUser(user.id);
    return user;
  }
}

@Injectable()
class AccountService {
  constructor(
    @InjectTransaction()
    private readonly tx: Transaction<TransactionalAdapterPrisma>,
  ) {}

  async createAccountForUser(id: number): Promise<Account> {
    return this.tx.create({
      data: { userId: id, number: Math.random() },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Other ORMs

The @nestjs-cls/transactional library works in conjunction with database adapters, so an identical setup can be made with any other database library which supports callback-based transactions.

If there doesn't exist a ready-made adapter for your database library of choice, it's pretty straight-forward to create a custom one.

typeorm Article's
30 articles in total
Favicon
Creating Typescript app with decorator-based dependency injection 💉
Favicon
ORM and Migrating/Adding Data to MySql Database from MongoDb using TypeOrm in javaScript
Favicon
🌟 NestJS + Databases: Making Snake Case Seamless!🐍
Favicon
NestJS TypeORM and Multi-Tenancy
Favicon
Nx + TypeORM + NestJS + Migrations
Favicon
Handling TypeORM migrations in Electron apps
Favicon
How to manage multiple environments with dotenv and Databases config in NestJS
Favicon
Sveltekit + TypeScript + TypeORM + ESM
Favicon
Using TypeORM with TSX: A Smoother Development Experience
Favicon
Prisma or TypeORM ?
Favicon
Mock TypeORM Package
Favicon
Connecting a Serverless PostgreSQL Database (Neon) to NestJS Using the Config Service
Favicon
NestJS and TypeORM — Efficient Schema-Level Multi-Tenancy with Auto Generated Migrations: A DX Approach
Favicon
Getting Started with NestJS and TypeORM: A Beginner's Guide
Favicon
Migration - Module query with TypeORM version 0.3.x
Favicon
Double bind the foreign key to avoid unnecessary JOIN in TypeORM
Favicon
Handling Migrations on NestJS with TypeORM
Favicon
match all conditions in the first array and at least one condition for the second array typeorm
Favicon
Taming cross-service database transactions in NestJS with AsyncLocalStorage
Favicon
Announcing Version 2.0 of nestjs-DbValidator
Favicon
Optimizing SQL Queries by 23x!!!
Favicon
Building my own PostgresGUI with TypeORM+TypeGraphQl class generaion
Favicon
TypeORM | Query Builder
Favicon
NestJs에서 TypeORM을 사용하여 MySQL 연동, 2024-01-25
Favicon
Defining Custom Many-to-many Relationship in NestJS TypeORM.
Favicon
4. Building an Abstract Repository
Favicon
3. Building a Common Repository for Nest.js Microservices
Favicon
TypeORM - remove children with orphanedRowAction
Favicon
Migrating NestJS project with TypeORM to Prisma
Favicon
Authentication part 2 using NestJS

Featured ones: