Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Golang Transactional API design

I'm trying to follow Clean Architecture using Go. The application is a simple image management application.

I'm wondering how to best design the interfaces for my Repository layer. I don't want to combine all repository methods into one single big interface, like some examples I found do, I think in Go small interfaces are usually preferred. I don't think the usecase code concerning managing images needs to know that the repository also stores users. So I would like to have UserReader, UserWriter and ImageReader and ImageWriter. The complication is that the code needs to be transactional. There is some debate where transaction management belongs in Clean Architecture, but I think the usecase-layer needs to be able to control transactions. What belongs in a single transaction, I think, is a business rule and not a technical detail.

Now the question is, how to structure the interfaces?

Functional approach

So in this approach, I open a transaction, run the provided function and commit if there are no errors.

type UserRepository interface {
    func ReadTransaction(txFn func (UserReader) error) error
    func WriteTransaction(txFn func (UserWriter) error) error
}

type ImageRepository interface {
    func ReadTransaction(txFn func (ImageReader) error) error
    func WriteTransaction(txFn func (ImageWriter) error) error
}

Problems: No I can't easily write a user and an image in a single transaction, I would have to create an extra UserImageRepository interface for that and also provide a separate implementation.

Transaction as repository

type ImageRepository interface {
    func Writer() ImageReadWriter
    func Reader() ImageReader
}

I think this would be rather similar to the functional approach. It wouldn't solve the problem of combined use of multiple repositories, but at least would make it possible by writing a simple wrapper.

An implementation could look like this:

type BoltDBRepository struct {}
type BoltDBTransaction struct { *bolt.Tx }
func (tx *BoltDBTransaction) WriteImage(i usecase.Image) error
func (tx *BoltDBTransaction) WriteUser(i usecase.User) error
....

Unfortunately, If I implement the transaction methods like this:

func (r *BoltDBRepository) Writer() *BoltDBTransaction
func (r *BoltDBRepository) Reader() *BoltDBTransaction

because this does not implement the ImageRepository interface, so I'd need a simple wrapper

type ImageRepository struct { *BoltDBRepository }
func (ir *ImageRepository) Writer() usecase.ImageReadWriter
func (ir *ImageRepository) Reader() usecase.ImageReader

Transaction as a value

type ImageReader interface {
    func WriteImage(tx Transaction, i Image) error
}

type Transaction interface { 
    func Commit() error
}

type Repository interface {
    func BeginTransaction() (Transaction, error)
}

and a repository implementation would look something like this

type BoltDBRepository struct {}
type BoltDBTransaction struct { *bolt.Tx }

// implement ImageWriter
func (repo *BoltDBRepository) WriteImage(tx usecase.Transaction, img usecase.Image) error {
  boltTx := tx.(*BoltDBTransaction)
  ...
}

Problems: While this would work, I have to type assert at the beginning of each repository method which seems a bit tedious.

So these are the approaches I could come up with. Which is the most suitable, or is there a better solution?

like image 422
tobiasH Avatar asked Aug 18 '18 21:08

tobiasH


1 Answers

Repository is a representation of a place that keep your datas, so is an architectural element.

Transaction is a technical detail that resolve a non-functional requisit (atomic operations), so it must be used like an internal reference or private function in architectural element.

In this case, if your repository was written like:

type UserRepository interface {
    func Keep(UserData) error
    func Find(UUID) UserData
}

type ImageRepository interface {
    func Keep(ImageData) error
    func Find(UUID) ImageData
}

Transactional approach is an implementation details, so you can create an "implementation" of UserRepository and ImageRepository that is being used like an internal reference.

type UserRepositoryImpl struct {
    Tx Transaction
}

func (r UserRepository) func Keep(UserData) error { return r.Tx.On(...)} 
func (r UserRepository) func Find(UUID) UserData { return r.Tx.WithResult(...)}

In this way you can keep user and image in a single transaction too.

For example, if a client has references to userRepository and imageRepository and if it is responsible of userData and imageData and it also desires to keep both data on single transaction then:

//open transaction and set in participants
tx := openTransaction()
ur := NewUserRepository(tx)
ir := NewImageRepository(tx)
//keep user and image datas
err0 := ur.Keep(userData)
err1 := ir.Keep(imageData)
//decision
if err0 != nil || err1 != nil {
  tx.Rollback()
  return
}
tx.Commit()

This is clean, objective, and work fine in Onion Architecture, DDD and 3-layers architecture(Martin Fowler)!

In Onion Architecture:

  • Entities: user and image (without business rules)
  • Usecase: repository interface (application rules: keep user and image)
  • Controller: A/N
  • DB/Api: client, tx, repositories implementations
like image 115
Aristofanio Garcia Avatar answered Nov 05 '22 19:11

Aristofanio Garcia