Skip to content

Commit b17c214

Browse files
committedSep 21, 2021
refactor: removed CommandHandlerBase and simplified UnitOfWork
1 parent 0edc57b commit b17c214

File tree

10 files changed

+56
-73
lines changed

10 files changed

+56
-73
lines changed
 

‎README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Some CQS purists may say that a `Command` shouldn't return anything at all. But
261261

262262
Though, violating this rule and returning some metadata, like `ID` of a created item, redirect link, confirmation message, status, or other metadata is a more practical approach than following dogmas.
263263

264-
All changes done by `Commands` (or by events or anything else) across multiple aggregates should be saved in a single database transaction (if you are using a single database). This means that inside a single process, one command/request to your application usually should execute **only one** [transactional operation](https://en.wikipedia.org/wiki/Database_transaction) to save **all** changes (or cancel **all** changes of that command/request in case if something fails). This should be done to maintain consistency. To do that something like [Unit of Work](https://www.c-sharpcorner.com/UploadFile/b1df45/unit-of-work-in-repository-pattern/) or similar patterns can be used. Example: [create-user.service.ts](src/modules/user/commands/create-user/create-user.service.ts) - notice how it extends a `CommandHandler<UnitOfWork>` and gets a transactional repository from `this.unitOfWork`.
264+
All changes done by `Commands` (or by events or anything else) across multiple aggregates should be saved in a single database transaction (if you are using a single database). This means that inside a single process, one command/request to your application usually should execute **only one** [transactional operation](https://en.wikipedia.org/wiki/Database_transaction) to save **all** changes (or cancel **all** changes of that command/request in case if something fails). This should be done to maintain consistency. To do that something like [Unit of Work](https://www.c-sharpcorner.com/UploadFile/b1df45/unit-of-work-in-repository-pattern/) or similar patterns can be used. Example: [create-user.service.ts](src/modules/user/commands/create-user/create-user.service.ts) - notice how it gets a transactional repository from `this.unitOfWork`.
265265

266266
**Note**: `Command` is not the same as [Command Pattern](https://refactoring.guru/design-patterns/command), it is just a convenient name to represent that this object executes some state-changing action. Both `Commands` and `Queries` in this example are just simple objects that carry data between layers.
267267

@@ -414,7 +414,7 @@ An alternative approach would be publishing a `Domain Event`. If executing a com
414414

415415
Domain Events may be useful for creating an [audit log](https://en.wikipedia.org/wiki/Audit_trail) to track all changes to important entities by saving each event to the database. Read more on why audit logs may be useful: [Why soft deletes are evil and what to do instead](https://jameshalsall.co.uk/posts/why-soft-deletes-are-evil-and-what-to-do-instead).
416416

417-
All changes done by Domain Events (or by anything else) across multiple aggregates in a single process should be saved in a single database transaction to maintain consistency. Patterns like [Unit of Work](https://www.c-sharpcorner.com/UploadFile/b1df45/unit-of-work-in-repository-pattern/) or similar can help with that. Example: [src/modules/wallet/wallet.providers.ts](src/modules/wallet/wallet.providers.ts) - notice how `OnUserCreatedDomainEventHandler` has a factory for creating an instance of this class with included transactional repository.
417+
All changes done by Domain Events (or by anything else) across multiple aggregates in a single process should be saved in a single database transaction to maintain consistency. Patterns like [Unit of Work](https://www.c-sharpcorner.com/UploadFile/b1df45/unit-of-work-in-repository-pattern/) or similar can help with that.
418418

419419
**Note**: this project uses custom implementation for publishing Domain Events. Reason for not using [Node Event Emitter](https://nodejs.org/api/events.html) or packages that offer an event bus (like [NestJS CQRS](https://docs.nestjs.com/recipes/cqrs)) is that they don't offer an option to `await` for all events to finish, which is useful when making all events a part of a transaction. Inside a single process either all changes done by events should be saved, or none of them in case if one of the events fails.
420420

@@ -426,6 +426,11 @@ Examples:
426426
- [user-created.domain-event.ts](src/modules/user/domain/events/user-created.domain-event.ts) - simple object that holds data related to published event.
427427
- [create-wallet-when-user-is-created.domain-event-handler.ts](src/modules/wallet/application/event-handlers/create-wallet-when-user-is-created.domain-event-handler.ts) - this is an example of Domain Event Handler that executes some actions when a domain event is raised (in this case, when user is created it also creates a wallet for that user).
428428
- [typeorm.repository.base.ts](src/libs/ddd/infrastructure/database/base-classes/typeorm.repository.base.ts) - repository publishes all domain events for execution when it persists changes to an aggregate.
429+
- [typeorm-unit-of-work.ts](src/libs/ddd/infrastructure/database/base-classes/typeorm-unit-of-work.ts) - this ensures that everything is saved in a single transaction.
430+
- [unit-of-work.ts](src/infrastructure/database/unit-of-work/unit-of-work.ts) - here you create factories for specific Domain Repositories that are used in a transaction.
431+
- [create-user.service.ts](src/modules/user/commands/create-user/create-user.service.ts) - here we get a user repository from a `UnitOfWork` and execute a transaction.
432+
433+
**Note**: Unit of work is not required for some operations (for example queries or operations that don't cause any side-effects in other aggregates) so you may skip using a unit of work in this cases and just use a regular repository injected through a constructor instead of a repository from a unit of work.
429434

430435
To have a better understanding on domain events and implementation read this:
431436

‎src/infrastructure/database/unit-of-work/unit-of-work.module.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Global, Module } from '@nestjs/common';
1+
import { Global, Logger, Module } from '@nestjs/common';
22
import { UnitOfWork } from './unit-of-work';
33

4-
const unitOfWorkSingleton = new UnitOfWork();
4+
const unitOfWorkSingleton = new UnitOfWork(new Logger());
55

66
const unitOfWorkSingletonProvider = {
77
provide: UnitOfWork,

‎src/libs/ddd/domain/base-classes/command-handler.base.ts

-22
This file was deleted.

‎src/libs/ddd/domain/ports/unit-of-work.port.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export interface UnitOfWorkPort {
2-
init(correlationId: string): void;
32
execute<T>(
43
correlationId: string,
54
callback: () => Promise<T>,

‎src/libs/ddd/infrastructure/database/base-classes/typeorm-unit-of-work.ts

+19-20
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
import { Logger } from '@nestjs/common';
21
import { UnitOfWorkPort } from '@src/libs/ddd/domain/ports/unit-of-work.port';
32
import { EntityTarget, getConnection, QueryRunner, Repository } from 'typeorm';
43
import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel';
4+
import { Logger } from 'src/libs/ddd/domain/ports/logger.port';
55

66
export class TypeormUnitOfWork implements UnitOfWorkPort {
7-
private queryRunners: Map<string, QueryRunner> = new Map();
7+
constructor(private readonly logger: Logger) {}
88

9-
/**
10-
* Creates a connection pool with a specified ID.
11-
*/
12-
init(correlationId: string): void {
13-
if (!correlationId) {
14-
throw new Error('Correlation ID must be provided');
15-
}
16-
const queryRunner = getConnection().createQueryRunner();
17-
this.queryRunners.set(correlationId, queryRunner);
18-
}
9+
private queryRunners: Map<string, QueryRunner> = new Map();
1910

2011
getQueryRunner(correlationId: string): QueryRunner {
2112
const queryRunner = this.queryRunners.get(correlationId);
2213
if (!queryRunner) {
2314
throw new Error(
24-
'Query runner not found. UnitOfWork must be initiated first. Use "UnitOfWork.init()" method.',
15+
'Query runner not found. Incorrect correlationId or transaction is not started. To start a transaction wrap operations in a `execute` method.',
2516
);
2617
}
2718
return queryRunner;
@@ -37,28 +28,36 @@ export class TypeormUnitOfWork implements UnitOfWorkPort {
3728

3829
/**
3930
* Execute a UnitOfWork.
40-
* Database operations wrapped in a UnitOfWork will execute in a single
41-
* transactional operation, so everything gets saved or nothing.
31+
* Database operations wrapped in a `execute` method will run
32+
* in a single transactional operation, so everything gets
33+
* saved (including changes done by Domain Events) or nothing at all.
4234
*/
4335
async execute<T>(
4436
correlationId: string,
4537
callback: () => Promise<T>,
4638
options?: { isolationLevel: IsolationLevel },
4739
): Promise<T> {
48-
const logger = new Logger(`${this.constructor.name}:${correlationId}`);
49-
logger.debug(`[Starting transaction]`);
50-
const queryRunner = this.getQueryRunner(correlationId);
40+
if (!correlationId) {
41+
throw new Error('Correlation ID must be provided');
42+
}
43+
this.logger.setContext(`${this.constructor.name}:${correlationId}`);
44+
const queryRunner = getConnection().createQueryRunner();
45+
this.queryRunners.set(correlationId, queryRunner);
46+
this.logger.debug(`[Starting transaction]`);
5147
await queryRunner.startTransaction(options?.isolationLevel);
48+
// const queryRunner = this.getQueryRunner(correlationId);
5249
let result: T;
5350
try {
5451
result = await callback();
5552
} catch (error) {
5653
try {
5754
await queryRunner.rollbackTransaction();
55+
this.logger.debug(
56+
`[Transaction rolled back] ${(error as Error).message}`,
57+
);
5858
} finally {
5959
await this.finish(correlationId);
6060
}
61-
logger.debug(`[Transaction rolled back] ${(error as Error).message}`);
6261
throw error;
6362
}
6463
try {
@@ -67,7 +66,7 @@ export class TypeormUnitOfWork implements UnitOfWorkPort {
6766
await this.finish(correlationId);
6867
}
6968

70-
logger.debug(`[Transaction committed]`);
69+
this.logger.debug(`[Transaction committed]`);
7170

7271
return result;
7372
}

‎src/modules/user/commands/create-user/create-user.cli.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class CreateUserCliController {
3838
street,
3939
});
4040

41-
const id = await this.service.executeUnitOfWork(command);
41+
const id = await this.service.execute(command);
4242

4343
this.logger.log('User created:', id.value);
4444
}

‎src/modules/user/commands/create-user/create-user.graphql-resolver.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class CreateUserGraphqlResolver {
1919
async create(@Args('input') input: CreateUserRequest): Promise<IdResponse> {
2020
const command = new CreateUserCommand(input);
2121

22-
const id = await this.service.executeUnitOfWork(command);
22+
const id = await this.service.execute(command);
2323

2424
return new IdResponse(id.value);
2525
}

‎src/modules/user/commands/create-user/create-user.http.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class CreateUserHttpController {
3030
async create(@Body() body: CreateUserHttpRequest): Promise<IdResponse> {
3131
const command = new CreateUserCommand(body);
3232

33-
const id = await this.service.executeUnitOfWork(command);
33+
const id = await this.service.execute(command);
3434

3535
return new IdResponse(id.value);
3636
}

‎src/modules/user/commands/create-user/create-user.message.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class CreateUserMessageController {
1717
async create(message: CreateUserMessageRequest): Promise<IdResponse> {
1818
const command = new CreateUserCommand(message);
1919

20-
const id = await this.service.executeUnitOfWork(command);
20+
const id = await this.service.execute(command);
2121

2222
return new IdResponse(id.value);
2323
}

‎src/modules/user/commands/create-user/create-user.service.ts

+24-22
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,38 @@ import { ConflictException } from '@libs/exceptions';
44
import { Address } from '@modules/user/domain/value-objects/address.value-object';
55
import { Email } from '@modules/user/domain/value-objects/email.value-object';
66
import { UnitOfWork } from '@src/infrastructure/database/unit-of-work/unit-of-work';
7-
import { CommandHandler } from '@src/libs/ddd/domain/base-classes/command-handler.base';
87
import { CreateUserCommand } from './create-user.command';
98
import { UserEntity } from '../../domain/entities/user.entity';
109

11-
export class CreateUserService extends CommandHandler<UnitOfWork> {
12-
protected async execute(command: CreateUserCommand): Promise<ID> {
10+
export class CreateUserService {
11+
constructor(protected readonly unitOfWork: UnitOfWork) {}
12+
13+
async execute(command: CreateUserCommand): Promise<ID> {
1314
/* Use a repository provided by UnitOfWork to include everything
1415
(including changes caused by Domain Events) into one
1516
atomic database transaction */
16-
const userRepo: UserRepositoryPort = this.unitOfWork.getUserRepository(
17-
command.correlationId,
18-
);
19-
// user uniqueness guard
20-
if (await userRepo.exists(command.email)) {
21-
throw new ConflictException('User already exists');
22-
}
23-
24-
const user = UserEntity.create({
25-
email: new Email(command.email),
26-
address: new Address({
27-
country: command.country,
28-
postalCode: command.postalCode,
29-
street: command.street,
30-
}),
31-
});
17+
return this.unitOfWork.execute(command.correlationId, async () => {
18+
const userRepo: UserRepositoryPort = this.unitOfWork.getUserRepository(
19+
command.correlationId,
20+
);
21+
// user uniqueness guard
22+
if (await userRepo.exists(command.email)) {
23+
throw new ConflictException('User already exists');
24+
}
3225

33-
user.someBusinessLogic();
26+
const user = UserEntity.create({
27+
email: new Email(command.email),
28+
address: new Address({
29+
country: command.country,
30+
postalCode: command.postalCode,
31+
street: command.street,
32+
}),
33+
});
3434

35-
const created = await userRepo.save(user);
35+
user.someBusinessLogic();
3636

37-
return created.id;
37+
const created = await userRepo.save(user);
38+
return created.id;
39+
});
3840
}
3941
}

0 commit comments

Comments
 (0)
Failed to load comments.