marcusolsson / goddd

Exploring DDD in Go
MIT License
2.39k stars 275 forks source link

Database transactions #32

Open adamfdl opened 4 years ago

adamfdl commented 4 years ago

Where should I put database transactions. Let's say I am building an e-commerce website, and I want to create a feature to order an item with a third-party payment. The simplified flow:

  1. Deduct item stock in database
  2. Charge to Paypal
  3. Create invoice in database

Where should I put the transaction implementation? It should not be in the repository layer no?

xescugc commented 4 years ago

Transactions with this model are hard, what I do is create a Unit of Work implementation, in which you pass a list of repositories that have to work together, and basically behind the scene create a TX for all of them, so all the calls, on that context, to the Repositories will be done on a TX.

This allows to have different TX (MySQL, FS) and other implementations of the repositories you have implement it's own way of having TX if any.

type StartUnitOfWork func(ctx context.Context, uowFn func(uow UnitOfWork) error, repositories ...interface{}) error
type UnitOfWork interface { 
  Users user.Repository
}
err := s.startUnitOfWork(ctx, func(uow unitwork.UnitOfWork) error { 
  uow.Users().Create(...)
}, s.users)

This is what I use and it works really good for me, I had to invest a big amount of time to find a good implementation that would fit the patterns ans till have separation without attaching the logic to an specific repository implementation (MySQL for example).

This is just an small example but to give ideas on how I solved it.

EDIT: This is only for same MS TX, for multiple MS TX then you need something totally different and much more hard to do (normally involving queues an potentially manual locks) but I would not invest time on that except if it's mandatory for you to do it but there are ways too https://microservices.io/patterns/data/transactional-outbox.html

adamfdl commented 4 years ago

Hi @xescugc, I haven't thought of that, and that is a really cool implementation. A couple of questions, where is startUnitOfWork()'s function being declared? Is it in the domain? Do you have a sample application that you can share?

fr3fou commented 4 years ago

@marcusolsson what's your way of handling this? I managed to think of a solution but it involes leaking an abstraction of my underlying store

type UserStore interface {
    Save(tx sql.Tx, u *User) error
}

this works but I'm constraining myself to sql stores only :/

xescugc commented 4 years ago

@adamfdl Sorry I lost the notification, this will also help @fr3fou.

I have one example of the Unit of Work that I was mentioning. The definition is https://github.com/xescugc/rebost/blob/master/uow/unit_of_work.go and the implementation is https://github.com/xescugc/rebost/blob/master/boltdb/unit_of_work.go for boltdb and https://github.com/xescugc/rebost/blob/master/fs/unit_of_work.go for filesystem. Basically what this allows is the Domain to be abstracted of the implementation behind a Repository, just make a UoW and be able to work.

I have MySQL implementation too but are from work so I can not release the info hehe but with those 2 examples you get the idea.

With this 2 implementation I can have the domain logic deal with the Repositories, and they deal with the implementation for rollbacks and transactions (the fs one is manual but it's the same Idea), you can see them in use in https://github.com/xescugc/rebost/blob/master/volume/volume.go#L348