简体   繁体   中英

Transaction in DDD design pattern advice

As we know service (controller/usecase) layer is to handle business logic, repo is to handle db queries

Right now I have:

func (s *OrderService) Create(order models.Order) (models.Order, error) {
  ...
  user := models.User{
    Contact: order.Address.Contact,
  }
  createdUser, err := s.UserRepo.Save(user)   
  // err handling...

  order.User = user
  createdOrder, err := s.OrderRepo.save(order)
  // err handling...

  return order, nil
}
// user_repo.go
func (repo *UserRepo) Save(user models.User) (models.User, error) {
  err := repo.DB.Debug().Save(&user).Error
  // err handing...
  return user, nil
}

// order_repo.go
func (repo *OrderRepo) Save(order models.Order) (models.Order, error) {
  err := repo.DB.Debug().Save(&order).Error
  // err handing...
  return order, nil
}

I want apply gorm db.Begin() transaction to become more flexible instead of my current code is too static. So should I remove the gorm.DB repo but

i. passing in the gorm.DB through params??

tx := s.DB.Begin()
createdUser, err := s.UserRepo.Save(user, tx)

ii. or straight away running query in service layer?? (but it broke ddd design concept)

tx := s.DB.Begin()
createdUser, err := tx.Create(&user)
if err != nil {
  tx.Rollback()
}
createdOrder, err := tx.Create(&order)  
if err != nil {
  tx.Rollback()
}
tx.Commit()

As per DDD, Transactions should not cross aggregate boundaries.

References:

If we have a need to update them in a transaction for some reason, you might wanna relook if they should be part of some Aggregate

While writing the repository for aggregate, you can neatly hide the transactions in the repository layer

I usually follow the following interface

// holds the business logic to modify the aggregate, provided by business layer
type AggregateUpdateFunction func (a *Aggregate) error
    
type Repository interface {
      Create(ctx context.Context, aggregate *Aggregate)
      Read(ctx context.Context, id string) *Aggregate
      // starts a read-modify-write cycle internally in a transaction   
      Update(ctx context.Context, id string, updateFunc AggregateUpdateFunction) error
}

GORM calls should definitely stay abstracted away in storage layer. If you leak implementation details like transaction handle to business logic, storage layer will become tightly coupled with that particular implementation of storage.

In domain-driven world one should probably model interface of storage layer in such way that it has all operations business logic needs to operate with domain objects rather than basic operations underlying database offers (point is if you later switch from SQL database to S3 REST API the interface towards business logic will stay same). So instead (or on top) of OrderRepo.Save() I would also create OrderRepo.SaveAsNewUser() (Order, User, err) which will internally leverage database transaction.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM