sairyss / domain-driven-hexagon Goto Github PK
View Code? Open in Web Editor NEWLearn Domain-Driven Design, software architecture, design patterns, best practices. Code examples included
License: MIT License
Learn Domain-Driven Design, software architecture, design patterns, best practices. Code examples included
License: MIT License
In the current solution, if a new property is added to UserModel, such as a password or credit card number, it will be returned and cause a data leak
This can be prevented by using the UserMapper toResponse method
Hi, thanks for the project, it is very good learning material.
I have a quick question: in the user module you have business rules on the property country of the VO address:
And later, in the UC create-user you have a DTO model that also species (different) validation rules:
Isn't that duplication problematic? Do you think we should try to keep both set of rules similar? Or the DTO rules are just a basic safety and only the Domain rules matter? And in any case: shouldn't the DTO rules be at least less restrictive than the Domain rules? Here it would not be possible to have a country between 30 and 50 characters.
Thanks again for your dedication.
@slonik/migrator is not compatible with slonik 30+, so you cannot create a migration.
To reproduce just delete node_modules folder and try to npm install packages.
Hi @Sairyss
First of all, I would like to thank you for that awesome repository ๐
As a beginner in Hexagonal architecture and DDD, your repository and all the documentation you wrote in the README.md
really helps to wrap my mind about concepts and good practices to build a clean modular monolith.
I guess I'm not the only one in the TS/JS community really appreciating the effort you put in that repository, so in order to go further and involve more community members I would like to propose to enable Github Discussions in that repository instead of opening issues for clarifying or requesting advices.
Let me know what you think ?
Thanks again
Hello !
The resolver should return
return match(result, {
Ok: (id: string) => new IdResponse(id),
Err: (error: Error) => {
if (error instanceof UserAlreadyExistsError)
throw new ConflictHttpException(error.message);
throw error;
},
});
instead of
Ok: (id: string) => new IdResponse(id)
Can you provide some useful examples for Adapters? If we integrate with call some 3rd party API we use Adapters?
Providers using as NestJS providers or what?
Unpack method of value objects doesn't return the correct types for nested object values. Typescript shows as if an unpacked nested value object is still a value object.
First of all, I absolutely love this project, and am working to implement these patterns in production.
Anyway, to continue on the concept of what an "Aggregate Root" is, it can often be a "parent" entity that has relations containing one or many "child" entities (or Aggregates) that the parent has control or can act on. Since we are separating Domain Entities from ORM entities, we will need a way for an Aggregate Root to gain context of the child relations in an ORM-agnostic way.
I borrowed some of the naming conventions from Mikro ORM, prefixed with Domain
to avoid naming collisions and better segregate the mental boundary.
Proposal:
For an x-to-many relation:
export interface DomainCollection<Child extends Entity<any>> {
load(): Promise<Child[]>
add(entity: Child): void
addMany(entities: Child[]): void
remove(entity: Child): void
removeMany(entities: Child[]): void
update(entities: Child[]): void
}
For an x-to-one relation:
export interface DomainReference<Child extends Entity<any>> {
load(): Promise<Child>
update(updatedEntity: Child): void
remove(): void
}
And a first stab at an implementation of the DomainCollection:
interface LoadStrategy<Parent, Child> {
load(p: Parent): Promise<Child[]>
}
export class Collection<Parent extends Entity<any>, Child extends Entity<any>>
implements DomainCollection<Child>
{
constructor(parent: Parent) {
this.#parent = parent
}
readonly #parent: Readonly<Parent>
#loadStrategy?: LoadStrategy<Parent, Child>
#items?: Map<ID, Child>
update(entities: Child[]): void {
entities.forEach((entity) => {
this.items.set(entity.id, entity)
})
}
add(entity: Child): void {
const existing = this.items.get(entity.id)
if (existing)
throw new DomainException('Child entity with id already exists')
this.items.set(entity.id, entity)
}
addMany(entities: Child[]): void {
entities.forEach((entity) => {
const existing = this.items.get(entity.id)
if (existing)
throw new DomainException('Child entity with id already exists')
this.items.set(entity.id, entity)
})
}
remove(entity: Child): void {
this.items.delete(entity.id)
}
removeMany(entities: Child[]): void {
entities.forEach((entity) => {
this.items.delete(entity.id)
})
}
withLoadStrategy(strategy: LoadStrategy<Parent, Child>) {
this.#loadStrategy = strategy
}
async load() {
if (!this.#loadStrategy)
throw new Error('Load strategy has not been provided!')
if (!this.#items) {
const records = await this.#loadStrategy.load(this.#parent)
this.#items = new Map(records.map((child) => [child.id, child]))
}
return Array.from(this.#items.values())
}
get items() {
if (!this.#items) throw new Error('Collection has not been hydrated')
return this.#items
}
}
Thoughts with this approach:
Would love to see your take on this idea...
Any idea on inside what layer authentication & authorization should be handled?, what is the best way to do that?.
my current implementation is that i have a UserGuard
port which is basically an interface with the following methods:
import GuardError from './errors'
interface UserGuard {
createKeyFor(user: User): Promise<string>
findKeyHolder(key: string): AsyncOutcome<User, GuardError>
}
The testing implementation uses a Map
object and an in-memory implementation of UserRepo
it map access keys to user aggregate id values, the production implementation will use an actual caching store such as redis and the user database table.
The "access key" is essentially a typical session cookie, but it could be a JWT token or something else the management of these tokens is all up to the underlying implementation, domain and application layers are blind to this.
The UserGuard
lives inside the identity subdomain of my application, it's where user identity data is managed other subdomains (for example Shipping & Billing subdomains) have their own guard ports that are very similar to UserGuard
and the production implementation will use the same redis store, basically the user will log in and create an access key, the user then could use the access key to interact with the identity subdomain as a User
or the billing subdomain as a Customer
.
is this a good way of doing things ?, also where should authorization be done?, I'm doing most of it in the domain layer for example I'd have a domain service called payInvoice(customer: Customer, invoice: Invoice): Outcome<Invoice, Error>
this service will return a failed outcome if the invoice belongs to a different customer or if the customer was denied the right to pay any invoices.
I think this repo should add a bit more information around authentication and where and maybe how it should be done.
Let's say we have two concepts: Book
and BookBeingSold
. Book
have methods like read
, rate
, and have properties like title
and author
. BookBeingSold
have the same methods and properties but it also have additional methods buy
, cancel
, changePrice
, and property price
. The database have two tables books
(id, title, author) and books-being-sold
(id, book_id, price, is_deleted).
What should code look like for this scenario in DDD way? Should we create two separate modules book
and bookBeingSold
and two separate repositories? Should we inherit one module from another? Or we should just have one module book
and one repository?
What is the best approach for multi-tenancy on TypeORM?
Currently, I created a TenantModule (Credits to Esposito Medium post to create and change de TypeORM connection on the fly.
But this doesn't seem right, since this ties the TypeORM connection and other sources to a general module instead of let each data source handle its way to multitenancy.
What do you think is the best practice for this?
What will be your approach for adding optional parameters to the user entity that you do not want to be added via the constructor?
Because it seems like the props
value in the base entity only represents items added via the constructor and would not accept any other value otherwise.
First off, thank you for this excellent example. I have learned a lot from it!
I'm encountering an intermittent test failure for the create-user
e2e spec.
~/C/P/domain-driven-hexagon master = !3 ?2 โฏ yarn test 4s 06:41:20 AM
yarn run v1.22.19
$ jest --config .jestrc.json
PASS tests/user/delete-user/delete-user.e2e-spec.ts (12.032 s)
FAIL tests/user/create-user/create-user.e2e-spec.ts (12.212 s)
โ Create a user โบ I can create a user
expect(received).toBe(expected) // Object.is equality
Expected: "string"
Received: "undefined"
42 | then('I receive my user ID', () => {
43 | const response = ctx.latestResponse as IdResponse;
> 44 | expect(typeof response.id).toBe('string');
| ^
45 | });
46 |
47 | and('I can see my user in a list of all users', async () => {
at Object.stepFunction (tests/user/create-user/create-user.e2e-spec.ts:44:34)
at node_modules/jest-cucumber/src/feature-definition-creation.ts:134:65
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 6 passed, 7 total
Snapshots: 0 total
Time: 12.906 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
When I log out the result
monad at the controller for that particular endpoint, I see what might be a clue.
When all tests pass, I see the conflict monad printed to the console, then the successful monad that contains the user entity id
~/C/P/domain-driven-hexagon master = !3 ?2 โฏ yarn test 17s 06:42:58 AM
yarn run v1.22.19
$ jest --config .jestrc.json
PASS tests/user/delete-user/delete-user.e2e-spec.ts (9.954 s)
โ Console
console.log
ResultType {
[Symbol(Val)]: UserAlreadyExistsError: User already exists
at CreateUserService.execute (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.service.ts:39:20)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at CreateUserHttpController.create (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.http.controller.ts:42:7) {
cause: ConflictException [Error]: Record already exists
at UserRepository.insert (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/libs/db/sql-repository.base.ts:118:15)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at /Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/libs/db/sql-repository.base.ts:215:24
at execTransaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/connectionMethods/transaction.js:19:24)
at transaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/connectionMethods/transaction.js:77:16)
at /Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/binders/bindPool.js:120:24
at createConnection (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/factories/createConnection.js:111:18)
at Object.transaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/binders/bindPool.js:119:20)
at CreateUserService.execute (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.service.ts:35:7)
at CreateUserHttpController.create (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.http.controller.ts:42:7) {
cause: [UniqueIntegrityConstraintViolationError],
metadata: undefined,
correlationId: 'r0aTGk',
code: 'GENERIC.CONFLICT'
},
metadata: undefined,
correlationId: 'r0aTGk',
code: 'USER.ALREADY_EXISTS'
},
[Symbol(T)]: false
} result
at CreateUserHttpController.create (src/modules/user/commands/create-user/create-user.http.controller.ts:44:13)
PASS tests/user/create-user/create-user.e2e-spec.ts (10.035 s)
โ Console
console.log
ResultType {
[Symbol(Val)]: '9ed7cc92-5903-4107-a6e3-f1cf7536eb1d',
[Symbol(T)]: true
} result
at CreateUserHttpController.create (src/modules/user/commands/create-user/create-user.http.controller.ts:44:13)
Test Suites: 2 passed, 2 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 10.585 s, estimated 13 s
Ran all test suites.
โจ Done in 15.33s.
However, when the test fails, I see the success monad printed first, then the conflict monad
~/C/P/domain-driven-hexagon master = !2 ?2 โฏ yarn test 16s 06:51:45 AM
yarn run v1.22.19
$ jest --config .jestrc.json
PASS tests/user/delete-user/delete-user.e2e-spec.ts
โ Console
console.log
ResultType {
[Symbol(Val)]: '4d479c18-2f8a-4fd8-a4cc-8ae30e3ce6a3',
[Symbol(T)]: true
} result
at CreateUserHttpController.create (src/modules/user/commands/create-user/create-user.http.controller.ts:44:13)
FAIL tests/user/create-user/create-user.e2e-spec.ts (5.084 s)
โ Console
console.log
ResultType {
[Symbol(Val)]: UserAlreadyExistsError: User already exists
at CreateUserService.execute (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.service.ts:39:20)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at CreateUserHttpController.create (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.http.controller.ts:42:7) {
cause: ConflictException [Error]: Record already exists
at UserRepository.insert (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/libs/db/sql-repository.base.ts:118:15)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at /Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/libs/db/sql-repository.base.ts:215:24
at execTransaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/connectionMethods/transaction.js:19:24)
at transaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/connectionMethods/transaction.js:77:16)
at /Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/binders/bindPool.js:120:24
at createConnection (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/factories/createConnection.js:111:18)
at Object.transaction (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/node_modules/slonik/dist/src/binders/bindPool.js:119:20)
at CreateUserService.execute (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.service.ts:35:7)
at CreateUserHttpController.create (/Users/zacharyweidenbach/Code/Personal/domain-driven-hexagon/src/modules/user/commands/create-user/create-user.http.controller.ts:42:7) {
cause: [UniqueIntegrityConstraintViolationError],
metadata: undefined,
correlationId: 'DkMKdX',
code: 'GENERIC.CONFLICT'
},
metadata: undefined,
correlationId: 'DkMKdX',
code: 'USER.ALREADY_EXISTS'
},
[Symbol(T)]: false
} result
at CreateUserHttpController.create (src/modules/user/commands/create-user/create-user.http.controller.ts:44:13)
โ Create a user โบ I can create a user
expect(received).toBe(expected) // Object.is equality
Expected: "string"
Received: "undefined"
42 | then('I receive my user ID', () => {
43 | const response = ctx.latestResponse as IdResponse;
> 44 | expect(typeof response.id).toBe('string');
| ^
45 | });
46 |
47 | and('I can see my user in a list of all users', async () => {
at Object.stepFunction (tests/user/create-user/create-user.e2e-spec.ts:44:34)
at node_modules/jest-cucumber/src/feature-definition-creation.ts:134:65
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 6 passed, 7 total
Snapshots: 0 total
Time: 5.534 s, estimated 15 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
I'm looking into this more, but my first instinct is that either the tests are running in parallel and that is creating some race condition, although I don't see any jest arguments altering the worker count to enable parallel test execution. Alternatively there might be something wrong in the testing infrastructure for the response context and it being mutated in a non-deterministic way.
I was going through the code and noticed in the core package that you have these case-specific events defined
According to the standards, shouldn't these events be kept outside of core and within their own use-case-based modules?
using the structuredClone()
function, as it creates a deep copy of the object, which means it will lose any functions, getters, or setters that were defined on the original object. Instead, you will end up with a plain object that only contains the data properties.
When using thegetPropsCopy()
method to create a copy of an entity object, the StructuredClone()
function is converting the entity object into a plain object. This is causing issues when calling the getters for properties of the entity object.
For example, when trying to access the address.country
, address.postalCode
, and address.street
getters of a UserEntity object, the properties are returning undefined
due to the conversion of the object by StructuredClone().
This issue seems to be related to the StructuredClone()
function, and may be causing similar issues in other parts of the application.
public getPropsCopy(): EntityProps & BaseEntityProps {
const propsCopy = structuredClone({
id: this._id,
createdAt: this._createdAt,
updatedAt: this._updatedAt,
...this.props,
});
return Object.freeze(propsCopy);
}
ref to. >
toPersistence(entity: UserEntity): UserModel {
const copy = entity.getPropsCopy();
const record: UserModel = {
id: copy.id,
createdAt: copy.createdAt,
updatedAt: copy.updatedAt,
email: copy.email,
country: copy.address.country, // getter
postalCode: copy.address.postalCode, // getter
street: copy.address.street, // getter
role: copy.role,
};
return userSchema.parse(record);
}
getPropsCopy()
method to create a copy of the object.address.country
, address.postalCode,
and address.street
getters of the copied object.The address.country
, address.postalCode
, and address.street
getters should return the expected values.
The address.country
, address.postalCode
, and address.street
getters are returning undefined
.
All recommendations about use cases (application services) are clear when we deal with only one entities or aggregate.
When there are many entities you recommend to use domain service instead.
Use case contain all entity methods calls and the call to repository and persistence part.
If we use domain service instead, can we manage calls to repositories and the persistence part from the domain service ?
Or domain service will call use cases at the end ?
I don't know how to describe relations, so please let me know.
example:
individual-> telnumer [n]
How to register phone number information in the phone number table when creating an individual?
Looking into this find-users.query-handler.ts query handler i noticed you were using the user repository for fetching the users.
Since on the read side we're not looking into enforcing any business rules, like we are on the command/write side with Aggregates, is there a reason for using repositories?
For example if we were to have a Post
aggregate and Comment
entities within that aggregate there'd be no point in building out eachPost
and fetching all of its Comment
relations when fetching something like a list of all posts.
I couldn't find much on this online but i found this blog post which might explain this better than i can.
Btw, great work on the repo <3
Nice work and great documentation. I'm wondering about the entity.base.ts
constructor. It looks like a new ID is generated for an entity when the constructor is invoked. But there are cases where we want to instantiate an entity object that already has an ID (e.g. from persistence). Right?
From Eric Evans "DDD Reference"-pdf (2015). In the description of Repositories "...return fully instantiated objects or collections (encapsulate the storage technology) .... provide repositories only for aggregate roots keep application logic focused the model ...."
Thanks for this great repo, it's a mine of good practices and advice.
I've a remark about performance when we have relationship between entities.
We can have different cases , entities in same module or entities in same aggregate.
is there any recommendations to manage entities relationship and preserve good performance results?
Avoid to load all related entities, for example an user that have many posts and each action about post we should avoid to load all posts from user before to execute add, update or any.
Hi!
I'm just exploring your repo and I think I noticed a typo in the name of the file /src/modules/user/queries/find-users/find-users.gralhql-resolver.ts
. I guess it should say find-users.graphql-resolver.ts
. :)
Thanks for sharing your work!
You put together a terrific resource!
I am currently writing a book on backend development (https://zero2prod.com) and I'd be interested in featuring (and linking) to your diagram on Hexagonal architecture. While I was checking the repository though I didn't manage to find a license covering the material - can you clarify what can and cannot be done with what is inside this repository?
see code
Hi @Sairyss!
Thanks for this awesome repo! It's been super helpful. I did encounter an issue recently though -
According to the readme - queries should be part of the application layer. But when looking at find-users.query-handler.ts, we can see it's importing dependencies from an outer layer - the infrastructure layer (like slonik
and user.repository
).
Doesn't that break the dependency rule? Or am I missing something?
If queries are at the same layer as application services, shouldn't they be required to use ports to communicate to "the outside world" just like application services are required to?
Thanks!
Simon
First, and foremost, amazing work you've done. The explanation and diagrams are awesome. You did an impressive job by reviewing and integrating some of the most relevant architecture/software patterns. Impressive!
I'm working right now on a pretty complex project, that requires a big refactor and I'm researching some architecture alternatives. Based on that, I'm already coding something pretty similar to your suggestions, but I would like to ask you some questions:
That is something that intrigues me. I've read some sources recommending that events should be handled at the Domain layer. But other (the ones that I'm tempted to follow) suggest that the domain layer should not handle complexity such as events. Instead, they suggest handling those in the Application layer, consuming the Domain layer just for business logic. What is your opinion for that?
Since you suggest that queries should bypass the domain layer to use the repository, I'm assuming you're considering that read models should live in the application layer. Is that correct? What about write models?
core
, infrastructure
, models
). What do you think about grouping them based on their BC instead?For example, instead of having:
<root folder>
โโโ src
โ โโโ core
โ โ โโโ events
โ โ โ โโโ created-user.ts
โ โ โ โโโ dispatched-product.ts
โ โ โ โโโ ....
โ โ โโโ commands
โ โ โ โโโ create-user.ts
โ โ โ โโโ dispatch-product.ts
โ โ โ โโโ ....
โ โ โโโ ...
What do you think about having:
<root folder>
โโโ src
โ โโโ user
โ โ โโโ events
โ โ โ โโโ created-user.ts
โ โ โ โโโ ....
โ โ โโโ commands
โ โ โ โโโ create-user.ts
โ โ โ โโโ ....
โ โโโ product
โ โ โโโ events
โ โ โ โโโ dispatched-product.ts
โ โ โ โโโ ....
โ โ โโโ commands
โ โ โ โโโ dispatch-product.ts
โ โ โ โโโ ....
โ โ โโโ ...
I know it looks strange, but I've tested with this type of folder structure for a time now, and it scales pretty well. It is easier to navigate, and it usually makes your import paths smaller and easier to understand.
For the record, I'm also using 2 type of folder: lib
and vendor
. The first one I use for common, shared logic external from my core (integration with databases, abstract classes, and others). The second one (vendor) I use for framework-specific logic (like bootstrapping NestJS application, configuring TypeORM, and others).
I would like to hear your opinion about this as well :)
Again, great job :) looking forward for an answer.
In the graphql resolver we need to unwrap the id since the type of the service is : Promise<Result<AggregateID, ManipulatorAlreadyExistsError>>
Remplace resolver by :
@Resolver()
export class CreateManipulatorGraphqlResolver {
constructor(private readonly commandBus: CommandBus) {}
@Mutation(() => IdGqlResponse)
async create(
@Args('input') input: CreateManipulatorGqlRequestDto,
): Promise<IdGqlResponse> {
const command = new CreateManipulatorCommand(input);
const result = await this.commandBus.execute(command);
return new IdGqlResponse(result.unwrap());
}
}
Hi, in value-object.base.ts we define 3 types:
export type Primitives = string | number | boolean;
export interface DomainPrimitive<T = Primitives> {
value: T;
}
type ValueObjectProps<T> = T extends Primitives | Date ? DomainPrimitive<T> : T;
If T is a Primitives or a Date then ValueObjectProps will be a DomainPrimitive of T but T = Primitives
forbid T from being a Date. I think typescript should not let us write that. If we replace the "=" with an "extend" the problem is clear:
Use "extends" instead of "=" and either have DomainPrimitive accept T as a Date or a Primitives:
export interface DomainPrimitive<T extends Primitives | Date> {
value: T;
}
Or define a new type DomainDate
:
export interface DomainPrimitive<T extends Primitives> {
value: T;
}
export interface DomainDate<T extends Date> {
value: T;
}
type ValueObjectProps<T> = T extends Primitives ? DomainPrimitive<T> : T extends Date ? DomainDate<T> : T;
Tell me if I misunderstood something. Maybe the "=" as an importance I am not aware of?
I suggest that instead directly import UserRepository to @Inject with UserRepositoryPort, it still make the handler depend on repository, the port interface here make no use and just for view.
const repositories = [
{
provide: 'UserRepository',
useClass: UserRepository,
},
];
constructor(
@Inject('UserRepository') private readonly repository: UserRepositoryPort,
) {}
Now the @Inject use the class name UserRepository provided in user.module.ts to know exactly dependency to inject to service, no need to import UserRepository directly inside service file
I'm sorry if this isn't the right place to ask such a question, I've searched for hours everywhere and cannot seem to find an answer.
If i have a use case that fetches a two aggregates of different types from their repositories, and passes them to a domain service that preforms business logic on the aggregates and returns them to the use case, how would i save all the aggregates back to the database in a single transaction ?.
one solution i came up with is to have one of the repositories methods take the second aggregate as an argument to include in the database transaction:
interface IMemberRepo {
upgradeMemberToGold: (member: Member, payment: Payment) => Promise<void>
}
// or
interface IPaymentRepo {
markPaymentAsFulfilled: (payment: Payment, member: Member) => Promise<void>
}
if a member aggregate gets upgraded to gold there must be a payment aggregate with fulfilled: true
that exists, the above solution works but I'm not sure if it 100% adheres to DDD principles.
Hello,
First of all, thank you for your awesome work, this is very inspiring .
Thank you very much :)!
In one of the MRs, someone asked a question about an example of authorship. In 2021 you wrote that if you find time you will implement such an example. Is this up to date?
I'm interested in this topic and would like to see what it might look like with your eye.
First of all, thank you for this great repo.
When you talk about domain events, you note that you have implemented your own version because NestJS CQRS lacks await. Maybe Iโm misunderstanding, but I think the events are now reactive, so you can await them and even get a result https://docs.nestjs.com/recipes/cqrs#events
when excute start:dev it throws error
src/libs/ddd/infrastructure/database/base-classes/typeorm.repository.base.ts:47:47 - error TS2769: No overload matches this call.
Overload 1 of 4, '(entities: DeepPartial<OrmEntity>[], options?: SaveOptions | undefined): Promise<(DeepPartial<OrmEntity> & OrmEntity)[]>', gave the following error.
Argument of type 'OrmEntity' is not assignable to parameter of type 'DeepPartial<OrmEntity>[]'.
Overload 2 of 4, '(entity: DeepPartial<OrmEntity>, options?: SaveOptions | undefined): Promise<DeepPartial<OrmEntity> & OrmEntity>', gave the following error.
Argument of type 'OrmEntity' is not assignable to parameter of type 'DeepPartial<OrmEntity>'.
47 const result = await this.repository.save(ormEntity);
what am i missing?
Hi, I have been using your repository as a guide to develop an application and I must say your work is amazing. I've been trying to develop a library management system and whenever I find myself confused, I try to approach a solution similar to your work. However, I am having difficulties in something you haven't covered yet.
I am trying to define a many-to-many relationship between OrmEntities ( in my case book and author ). In my domain entities, I have a book entity that has as attribute an array of author entity, so when I am trying to map the props of book to Orm props, I find myself needing to map also the props of author ( which has already its own mapper ). What I have implemented currently is that I use the author mapper in the book mapper to map the props of the author.
I was wondering how should I approach mapping between entities. Is calling a mapper inside another mapper the correct approach?
Thank you and looking forward for your answer!
Hi, thank you for this great repo.
I have two questions.
query getUser($filterUser: GetUserFilter!, $filterWallet: GetFilterWallet!) {
getUser(filter: $filterUser) {
id
wallets(filter: $filterWallet) {
id
}
}
}
Just thought I'd take a moment to thank you for this awesome repo. I regularly struggle for technical sources online suitable for enterprise level development; this is one of the best I've seen on this subject.
One question comes to mind, how would you organise GraphQL within this space? Originally l considered a singular graphql model isolated... but that seems inelegant having thought on it.
Hi, your code is a true source of inspiration regarding ddd, lots of explanations, great job!
In your code there is only save and delete for the user. For update I have some issues regarding saving/updating the aggregate.
Using typeorm(in the end) the save/update method would update all aggregate(step 1) properties (not only the changed ones).
If we have two requests that change two different properties(firstName, lastName) and the first request has some delay/more work/etc, that means that the last request would be overwritten by the first. ( Somehow the repo shoud save only what changed in the aggregate?)
Thanks,
I've stumbled upon this repo by chance and I think it's great, thanks for this !
Not sure if I'm missing something but why are database ports such as user.repository.port.ts
placed in infrastructure layer next to its implementation?
Shouldn't it be defined in application/domain core where its injected?
Your uow (in my typeorm-unit-of-work.base.ts) class creates many connections and block any query to the database.
import { EntityTarget, getConnection, QueryRunner, Repository } from 'typeorm';
import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel';
import { ILoggerPort, IUnitOfWorkPort } from 'src/shared/domain/ports';
import { ServiceResponseDtoBase } from 'src/shared/interface-adapters/base-classes/service-response-dto.base';
export class TypeORMUnitOfWork implements IUnitOfWorkPort {
constructor(private readonly logger: ILoggerPort) {}
private _queryRunners: Map<string, QueryRunner> = new Map();
// Use this and check in cron job (executed every 10 min)
getQueryRunners() {
return this._queryRunners;
}
getQueryRunner(correlationId: string): QueryRunner {
const queryRunner = this._queryRunners.get(correlationId);
if (!queryRunner) {
throw new Error(
'Query runner not found. Incorrect correlationId or transaction is not started. To start a transaction wrap operations in a "execute" method.',
);
}
return queryRunner;
}
getOrmRepository<Entity>(
entity: EntityTarget<Entity>,
correlationId: string,
): Repository<Entity> {
const queryRunner = this.getQueryRunner(correlationId);
return queryRunner.manager.getRepository(entity);
}
async execute<T>(
correlationId: string,
callback: () => Promise<T>,
options?: { isolationLevel: IsolationLevel },
): Promise<T> {
if (!correlationId) {
throw new Error('Correlation ID must be provided');
}
this.logger.setContext(`${this.constructor.name}:${correlationId}`);
const queryRunner = getConnection().createQueryRunner();
this._queryRunners.set(correlationId, queryRunner);
this.logger.debug(`[Starting transaction]`);
await queryRunner.startTransaction(options?.isolationLevel);
let result: T | ServiceResponseDtoBase<T>;
// Here i use my own wrapper, like from source
try {
result = await callback();
if (
ServiceResponseDtoBase.isError(
result as unknown as ServiceResponseDtoBase<T>,
)
) {
await this.rollbackTransaction(
correlationId,
(result as unknown as ServiceResponseDtoBase<any>).data,
);
return result;
}
} catch (error) {
await this.rollbackTransaction<T>(correlationId, error as Error);
throw error;
}
try {
await queryRunner.commitTransaction();
} finally {
await this.finish(correlationId);
}
this.logger.debug(`[Transaction committed]`);
return result;
}
private async rollbackTransaction<T>(correlationId: string, error: Error) {
const queryRunner = this.getQueryRunner(correlationId);
try {
await queryRunner.rollbackTransaction();
this.logger.debug(
`[Transaction rolled back] ${(error as Error).message}`,
);
} finally {
await this.finish(correlationId);
}
}
private async finish(correlationId: string): Promise<void> {
const queryRunner = this.getQueryRunner(correlationId);
try {
await queryRunner.release();
} finally {
this._queryRunners.delete(correlationId);
}
}
}
And in my cron job just inject this UoW and call getQueryRunners().size, so around 140 objects after 30 mins in production...
Any advice or there are reasons why?
While working through your database implementation, I was wondering why UserRepository
does not use TypeormRepositoryBase.findOne
?
domain-driven-hexagon/src/modules/user/database/user.repository.ts
Lines 35 to 43 in 0b55c3c
Shouldn't we do this instead:
const emailVO = new Email(email);
const user = await this.findOne({ email: emailVO });
return user;
And a second question: It seems like UserRepository.prepareQuery
removes all query parameters except for id
? Why?
domain-driven-hexagon/src/modules/user/database/user.repository.ts
Lines 61 to 70 in 0b55c3c
Hello, thanks for great repository.
I'm wondering how ordering shall be implemented with this interface?
export interface OrderBy {
[key: number]: -1 | 1;
}
How can I pass DTO to sort some column ASC/DESC in controller?
Not really an issue, but wanna congratulate you on this repo, it's very good content.
I'm glad more ppl is walking the path I walked some years ago, even with a repository on github like I did .
Although I must say your diagram looks a lot better than mine :)
If you are curious, you can also check out my talk about it.
Keep it up!
Thanks a lot for this repo !
I would like to know how it would be possible to use a mapper with Prisma (like orm-mapper.base.ts).
As far as I understand, it sounds that it's not meant for creating model inside a class, but only inside a schema.prisma file
Thanks for any help !
hello, I'm in process of exploring this wonderful repo and I really hope that you will continue to work on it until it becomes a real-world example project I could always come back to.
But there are some things a bit confusing for me, mostly the "what should be where". Some stuff I have kind of understood but this is still confusing for me. For example, domain-event-handlers and email are under modules but they don't seem to contain any kind of code that would relate to modules, so why are they there?
Understandably, email looks like a work in progress but the directory is missing the modules part which would help me to tie it together. But the domain-event-handlers seem out of place for me.
Also, is there somewhere a reason why the lint rules are what they are?
I'm also having trouble setting up the dev environment. specifically, when I start the DB docker and then run start:dev, I get an exception that test-db doesn't exist.
now that I'm thinking about it, I can't seem to find any instructions on how to start the dev env. like, step-by-step what should be done and what do I need before I can run it.
PS: it seems that while adding a licence, you forgot to update it in package.json, but I doubt it has any significance since it's not in the npm registry.
First of all: Thank you for publishing this! Amazing job :)
I am wondering how to manage the Entity's updatedAt
timestamp. Should I set it manually in every method of every Entity subclass that performs an update?
For example, when updating the UserEntity
address:
domain-driven-hexagon/src/modules/user/domain/entities/user.entity.ts
Lines 45 to 50 in 0b55c3c
this._updatedAt = DateVO.now();
?Thanks so much for effort writing all this, this repo has been my go to for helping me grok DDD books and their concepts in the context of something tangible.
I'm working through your example implementation and noticed that at some point Nest upgraded their Logger implementation, so the Logger.setContext()
method is no longer available. Instead you're expected to pass the context as an optional second parameter in the log call.
All I can think is to add the log method overloads to the Logger port interface and then add a private class attribute for the logContext wherever you've previously called setContext()
.
Is there a cleaner way to deal with this that I'm missing?
when excute start:dev it throws error
src/libs/ddd/infrastructure/database/base-classes/typeorm.repository.base.ts:47:47 - error TS2769: No overload matches this call.
Overload 1 of 4, '(entities: DeepPartial<OrmEntity>[], options?: SaveOptions | undefined): Promise<(DeepPartial<OrmEntity> & OrmEntity)[]>', gave the following error.
Argument of type 'OrmEntity' is not assignable to parameter of type 'DeepPartial<OrmEntity>[]'.
Overload 2 of 4, '(entity: DeepPartial<OrmEntity>, options?: SaveOptions | undefined): Promise<DeepPartial<OrmEntity> & OrmEntity>', gave the following error.
Argument of type 'OrmEntity' is not assignable to parameter of type 'DeepPartial<OrmEntity>'.
47 const result = await this.repository.save(ormEntity);
what am i missing?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.