Open TbotaPhantA opened 9 months ago
Thank you so much for the feedback @TbotaPhantA! I really appreciate that you detailed your thoughts. It's important to clarify that my example doesn't quite qualify as a production solution. The primary goal was to demonstrate how to develop a system using DDD concepts.
I'm particularly intrigued by point 1 regarding private fields. Could you please share an example of an export that makes fields accessible for mapping?
Regarding the outbox pattern, I agree that it can be extended for each aggregate. I'll expand the set of fields and rename everything soon.
Concerning CRUD, I also agree. I might even implement something similar in the near future through https://cloud.google.com/apis/design/custom_methods.
If you have any more ideas, I'd be happy to hear them! Feel free to submit pull requests as well.
I'll implement the example with private fields and send it to you later.
@TbotaPhantA Another question. Is correlationId a field for establishing a connection between different events? In our system, messageId has the type number, while correlationId has the type string. Or is it the ID of a request in correlationId?
@TbotaPhantA Another question. Is correlationId a field for establishing a connection between different events? In our system, messageId has the type number, while correlationId has the type string. Or is it the ID of a request in correlationId?
CorrelationId is the trace id. We can track how certain request was flowing through the system. We can search logs by correlationId and it will show us all the logs which were caused by particular request. So correlationId is usually requestId. correlationId is also necesssary for implementing sagas, because sagas are basically asynchronous request/response. You send a command via broker with correlationId and you're waiting for a response to come via broker. The response must contain the correlationId.
messageId is the identificator of the message. Because relay guarantees at least once
delivery it means that messages can potentially be duplicated and sent twice. These duplicated events will have the same messageId and it will be easier for consumers to recognize the duplicates and implement idempotent mechanisms.
@TbotaPhantA Thank you for the clarification! I'll be waiting for your implementation with private fields
Here is an example of the product entity with encapsulated data. (repository is unfinished it will evolve)
It is an event sourcing style entity which tracks events for the entity(ProductCreated, PriceAdjusted, ProductInfoUpdated, ProductRemoved). Everything is private, no setters, no getters. It's fully covered with unit tests.
I implemented it today and I'm in the little bit of a hurry, so feel free to refactor it. If you'll come up with a better way to organize it, let me know.
As a side note. I didn't decide yet, whether it's good or bad to store the entire
before
andafter
state of the entity in the event. Heard different opinions, but I added before and after states as an example
Here is the service which uses this entity https://github.com/TbotaPhantA/event-driven-loosely-coupled-monolith/blob/main/apps/backend/src/sales/application/product/services/adjustPrice.service.ts
Here are the unit tests: https://github.com/TbotaPhantA/event-driven-loosely-coupled-monolith/tree/main/apps/backend/test/unit/sales/domain/product
@TbotaPhantA, thank you so much for the example. I took your implementation of the export() and applied it in the accounting context (reports subdomain - https://github.com/zhuravlevma/typescript-ddd-architecture/tree/main/src/accounting/reports/report). I moved export() to abstract classes to avoid unnecessary logic in the domain layer and also changed the messages, more clearly separating them into events and commands. I also moved all of this behind abstract classes.
What do you think of the new implementation? Do you see any issues anywhere?
apply
method public and try to implement projection from event sourcing. Or here is the video explaining what apply method is and what projection is https://youtu.be/AUj4M-st3ic?list=PLThyvG1mlMzkRKJnhzvxtSAbY8oxENLUQ. All the business logic should be inside adjustPrice, updateProductInfo... methods, because these methods are command handlers, business logic should usually be in these command handlers, not in the apply method. Look at my apply methods. They're dead simple. Business invariants should also be in the command handlers.apply<EventName>
. If you have event called ReportValidated
you should name apply method applyReportValidated
@TbotaPhantA
I use the same style of event tracking even when I don't use event sourcing, It's probably more of a matter of taste.
Hey, I like how you implemented ideas from Domain-Driven Design and Clean Architecture, I'm in the process of doing the same thing, but before I implement my own version, I'm studying how other people implement it in typescript. After studying your repository I can add a little bit of criticism and suggestions to your implementation, I hope it will be helpful for you.
export
method for that purpose which returns raw object with encapsulated data, but this method won't be used in the business logic.debezium
to send messages from the outbox table. It doesn't necessarily have to be a relay. But relay is also okay, there's nothing wrong with that.event
orcommand
. The tuple which is located in the outbox table is calledMessage
. You also miss a little bit of data in the outbox table, therefore my preferred outbox naming would be:If I'll find something else I'll make sure to write about it.