Open timsolov opened 2 years ago
Hey @timsolov! Thanks, it's great to hear the project is helpful. 🙂
What you mention is correct, these two places don't have consistent updates. We did this on purpose to show how to refactor it later, but didn't come to this stage yet.
The short answer is, keep things you want to be atomic in a single transaction. So definitely in a single service, where you have control over it.
Another approach is using event-driven patterns: saving one entity in one of the services, and asynchronously updating the other one. In this case, you have to be able to accept eventual consistency.
If your application is just a single service it should be possible to keep database updates within transactions where applicable. Sadly this won't work for external calls — events work great for these. I've described one approach in this example: https://github.com/ThreeDotsLabs/watermill/tree/master/_examples/real-world-examples/transactional-events
Hi! Thank you for your work, your articles are really cool and useful!
The updateFn approach is awesome, but I'm not sure how to apply it across entities. I'm having difficulty putting 2 tables into one transaction.
I'll give you an example. I need to implement BuyProductCommand(customer, product)
where product
and customer
are two different business entities. How can i do this? It comes to mind to implement the Update
method, which will use a single updateFn
function with 2 arguments (one for customer
, and the second for product
), but then the question arises, to which repository should this method be assigned and what should it be called?
Is it generally correct to use this approach for multiple tables?
Hey @wintermonth2298!
This is a common question about this pattern, and the reason usually is thinking about the business entities as SQL tables and modeling the code around them. But they don't need to map 1:1, and bigger entities (aggregates) can easily span multiple tables.
I've described it in this article: https://threedots.tech/post/common-anti-patterns-in-go-web-applications/#starting-with-the-database-schema
The point is there's no rule that CustomerRepository
can't know about the Product
domain model (or table). If Product
is part of the Customer
model, the repository needs to know how to handle saving it.
You can consider having a regular Update()
method on the customer repository, fetching the product before running it, and calling the domain method of BuyProduct()
inside.
product, err := productsRepo.ByID(productID)
err := customerRepo.Update(ctx, func(customer *Customer) error {
customer.BuyProduct(product)
return nil
})
Or, consider a dedicated method like BuyProduct(customer, product)
directly on the repository if you also bump the number of bought copies on the product model. I'd say it's not a big deal which repository has this method then, it's an implementation detail anyway.
I hope this helps! As always, the context matters, and I don't know your domain well enough.
Thank you for your repo and articles. This is awesome work! I'm trying to rewrite my legacy service to the yours approach.
And I have a question:
How to guarantee that all hours in this command will be updated atomic?
https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/blob/33e3c49f825ad1e542de2bf3ab7da70af3b23bc3/internal/trainer/app/command/make_hours_available.go#L24
Or what will happen when repo will be updated here: https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/blob/master/internal/trainings/app/command/schedule_training.go#L56 but one of external service won't be available? Will we have incompleteness here?