Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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()
like image 440
wcl Avatar asked Oct 29 '25 20:10

wcl


2 Answers

As per DDD, Transactions should not cross aggregate boundaries.

References:

  • https://martinfowler.com/bliki/DDD_Aggregate.html
  • Defining Aggregate Boundaries

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
}
like image 199
rustedGeek Avatar answered Nov 01 '25 14:11

rustedGeek


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.

like image 26
blami Avatar answered Nov 01 '25 13:11

blami



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!