---
url: /core/application/unit-of-work.md
description: >-
  The Unit of Work pattern in TypeScript: run all the writes of one use case in
  a single transaction, so all of them are kept or none.
---

# Unit of Work

A unit of work runs the writes of one use case in one transaction: all of them are kept, or none.

## Why

Placing an order writes twice: the `Order` is saved, then its `OrderPlaced` event is added to the
[outbox](./outbox.md). If the process stops between the two, the order is placed and nobody hears
about it. If the second write fails, the first one stays.

::: tip The fix
The command handler runs its work inside `unitOfWork.run(…)`. Every write made inside belongs to the
same transaction. The work returns a [`Result`](../utilities/result.md): `ok` commits, `err` rolls
back.
:::

## How it works

`run` opens a transaction, runs the work, and reads its outcome:

```ts
return this.unitOfWork.run(async () => {
	await this.orders.save(order);
	await this.outbox.add(events);
	return ok();
});
```

The unit of work tracks nothing: it does not know which aggregates were loaded. The handler saves
each change explicitly, inside `run`.

## Where it fits

The unit of work wraps the whole PlaceOrder use case: loading the order, placing it, saving it and
adding its events to the outbox.

::: tip
One transaction per use case. A command handler never calls another one: two nested `run` calls
open two transactions.
:::

## API

```ts
import { Transaction, UnitOfWork } from "@alveolus/core";
// or: from "@alveolus/core/unit-of-work"
```

### Type parameters

```ts
abstract class UnitOfWork extends Port {
	run<T, E>(
		work: () => Promise<Result<T, E>>,
	): Promise<Result<T, E>>;
}
```

| Parameter | What it is | Constraint |
| --- | --- | --- |
| `T` | The value of a success. | inferred from `work` |
| `E` | The error of a failure. | inferred from `work` |

### `begin()` &#x20;

```ts
protected abstract begin(): Promise<Transaction>
```

Opens a transaction and returns it. `run` calls it once per call.

### `Transaction.commit()` &#x20;

```ts
abstract commit(): Promise<void>
```

Makes the writes of the transaction permanent.

### `Transaction.rollback()` &#x20;

```ts
abstract rollback(): Promise<void>
```

Discards the writes of the transaction.

### `run(work)`&#x20;

```ts
run<T, E>(
	work: () => Promise<Result<T, E>>,
): Promise<Result<T, E>>
```

Begins a transaction and runs `work`. Commits when it returns `ok`, rolls back when it returns
`err`, and rolls back then throws again when it throws.

::: warning Caveats

* The unit of work tracks nothing: the handler saves each aggregate explicitly inside `run`.
* A failure to commit is technical: it is thrown.
* Nested calls to `run` open nested transactions through `begin`. Avoid calling a command handler
  from another one.
  :::

## Usage

Build a unit of work in PostgreSQL. Each step shows the whole file: added lines are highlighted, replaced lines are struck out.

### 1. Choose how to share the connection

The repositories and the outbox must write on the connection of the transaction without receiving it: an `AsyncLocalStorage`, created once by the composition root, carries it.

### 2. Declare the adapter

The adapter extends `UnitOfWork` and receives the pool and the storage of the current connection. `run` is already written: you only open the transaction.

```ts [src/shared-kernel/driven/pg/adapters/pg-unit-of-work.adapter.ts]
import type { AsyncLocalStorage } from "node:async_hooks";

import { UnitOfWork } from "@alveolus/core";
import type { Pool, PoolClient } from "pg";

export class PgUnitOfWork extends UnitOfWork {
	constructor(
		private readonly pool: Pool,
		private readonly current: AsyncLocalStorage<PoolClient>,
	) {
		super();
	}
}
```

TypeScript now asks for `begin()`: the next step adds it.

### 3. Open a transaction

`begin` opens a transaction on a connection of the pool and returns how to end it. `Transaction` has no state of its own, so a plain object is enough: no second class.

```ts [src/shared-kernel/driven/pg/adapters/pg-unit-of-work.adapter.ts]
import type { AsyncLocalStorage } from "node:async_hooks";

import { UnitOfWork } from "@alveolus/core"; // [!code --]
import { type Transaction, UnitOfWork } from "@alveolus/core"; // [!code ++]
import type { Pool, PoolClient } from "pg";

export class PgUnitOfWork extends UnitOfWork {
	constructor(
		private readonly pool: Pool,
		private readonly current: AsyncLocalStorage<PoolClient>,
	) {
		super();
	}

	protected async begin(): Promise<Transaction> { // [!code ++]
		const client = await this.pool.connect(); // [!code ++]
		await client.query("BEGIN"); // [!code ++]
		return { // [!code ++]
			commit: async () => { // [!code ++]
				await client.query("COMMIT"); // [!code ++]
			}, // [!code ++]
			rollback: async () => { // [!code ++]
				await client.query("ROLLBACK"); // [!code ++]
			}, // [!code ++]
		}; // [!code ++]
	} // [!code ++]
}
```

### 4. Share the connection

So that every adapter called inside `run` writes in the same transaction, `begin` puts the connection in the storage they read.

```ts [src/shared-kernel/driven/pg/adapters/pg-unit-of-work.adapter.ts]
import type { AsyncLocalStorage } from "node:async_hooks";

import { type Transaction, UnitOfWork } from "@alveolus/core";
import type { Pool, PoolClient } from "pg";

export class PgUnitOfWork extends UnitOfWork {
	constructor(
		private readonly pool: Pool,
		private readonly current: AsyncLocalStorage<PoolClient>,
	) {
		super();
	}

	protected async begin(): Promise<Transaction> {
		const client = await this.pool.connect();
		await client.query("BEGIN");
		this.current.enterWith(client); // [!code ++]
		return {
			commit: async () => {
				await client.query("COMMIT");
			},
			rollback: async () => {
				await client.query("ROLLBACK");
			},
		};
	}
}
```

### 5. Release the connection

A connection that is never released exhausts the pool. Each way out of the transaction gives it back.

```ts [src/shared-kernel/driven/pg/adapters/pg-unit-of-work.adapter.ts]
import type { AsyncLocalStorage } from "node:async_hooks";

import { type Transaction, UnitOfWork } from "@alveolus/core";
import type { Pool, PoolClient } from "pg";

export class PgUnitOfWork extends UnitOfWork {
	constructor(
		private readonly pool: Pool,
		private readonly current: AsyncLocalStorage<PoolClient>,
	) {
		super();
	}

	protected async begin(): Promise<Transaction> {
		const client = await this.pool.connect();
		await client.query("BEGIN");
		this.current.enterWith(client);
		return {
			commit: async () => {
				await client.query("COMMIT");
				client.release(); // [!code ++]
			},
			rollback: async () => {
				await client.query("ROLLBACK");
				client.release(); // [!code ++]
			},
		};
	}
}
```

This is the complete unit of work.

### 6. Wire it and run in it

The composition root passes the same storage to the unit of work and to every adapter that writes in it:

```ts [src/app.module.ts]
const current = new AsyncLocalStorage<PoolClient>();
const unitOfWork = new PgUnitOfWork(pool, current);
const outbox = new PgOutbox(pool, current);
```

The [command handler](./command-handlers.md#usage) then wraps its work in `run`: a returned failure or a thrown error rolls everything back.

```ts [src/ordering/application/commands/place-order.command.ts]
return this.unitOfWork.run(async () => {
	const order = await this.orders.findById(new OrderId(orderId));
	…
	await this.orders.save(order);
	await this.outbox.add(events);
	return ok();
});
```

### 7. Check it

Run the checks. Two rules keep the unit of work the way it is now:

```sh
npx alveolus arch check
```

A transaction class declared in the same file is reported:

```
src/shared-kernel/driven/pg/adapters/pg-unit-of-work.adapter.ts:30
  tactical/no-misplaced-class: PgTransaction shares its file
  with PgUnitOfWork: one class per file.
```

## See also

* [Outbox](./outbox.md), written in the same transaction
* [Command handlers](./command-handlers.md), which run their work in it
* [Repositories](../domain/repositories.md), which write through it
* [Result](../utilities/result.md), whose outcome decides commit or rollback
* Rules: [`layers/no-portless-adapter`](../../rules/layers/no-portless-adapter.md), [`tactical/no-misplaced-class`](../../rules/tactical/no-misplaced-class.md)
