Memory and Thought Alex's home on the web

Transactions and the repository pattern in Go

A common challenge when implementing architectures which use the repository pattern is dealing with the requirement for transactions which need to coordinate multiple repositories. In this post I describe a simple architectural pattern to accomodate this.

The problem

Let’s imagine a web service which allows you to send out a drone to search for lost pets. You post a picture of your pet to an endpoint (let’s call it /petsearches) and a petsearch is created. Users can then GET /petsearches/<id> to get the status of the search. The code to create a search might look like the following:

func (i *PetSearchInteractor) createNewSearch(user *User, petPhoto []byte) (*PetSearch, error) {
    pet, err := i.PetRepository.CreatePet(petPhoto)
    if err != nil {
        return nil, err
    }
    search, err := i.SearchRepository.CreateSearch(pet)
    if err != nil {
        return nil, err
    }
    if err := i.searchScheduler.Schedule(search.id); err != nil {
        return nil, err
    }
    return search, nil
}

This code is using The Clean Architecture (an example of how to implement this in go can be found here). For the purposes of this blog post all you need to know is that the PetSearchInteractor is the part of the codebase which impements the business logic of the application.

This is all well and good, we create a pet, then we create a search which references the pet, and then we return the search. The problem arises if an error occurs during the saving of the search, we will be left with an orphaned pet, which is always a terrible thing.

What we would like to write would be something like the following:

func (i *PetSearchInteractor) createNewSearch(user *User, petPhoto []byte) (*PetSearch, error) {
    i.BeginTransaction()
    defer i.Complete()
    pet, err := i.PetRepository.CreatePet(petPhoto)
    if err != nil {
        return nil, err
    }
    search, err := i.SearchRepository.CreateSearch(pet)
    if err != nil {
        return nil, err
    }
    if err := i.searchScheduler.Schedule(search.id); err != nil {
        return nil, err
    }
    i.Commit()
    return search, nil
}

The intention here is that the Complete method of the PetSearchInteractor rolls back the transaction if it hasn’t been comitted, that way any errors which happen before a call to PetSearchInteractor.Commit() will result in the transaction being rolled back, the error reported to the client code and no orphaned pets, everyone is happy.

Decoupled code

Well not quite everyone, I’m not happy that the PetSearchInteractor is now responsible for tracking the committed/uncomitted state of the transaction, it makes the interactor harder to test.

I’m also not entirely sure how we would implement this, presumably the repositories operate on some sort of underlying connection pool, does the interactor tell each repository it owns to start a new transaction and then commit?

In the face of these problems we do what any good software engineer knows to do, we introduce another layer of indirection.

The PersistenceContext

One of the things I like about Go is that because it’s such a simple language it forces you to make things very explicit (which appeals to the Pythonista in me). The key here is that there is some context we want to execute all our repository related code in, let’s make this explicit in the code.

type PersistenctContextFactory interface {
    CreateContext() (PersistenceContext, error)
}

type PersistenceContext interface {
    func PetRepository() (PetRepository)
    func SearchRepository() (SearchRepository)
}

func (i *PetSearchInteractor) createNewSearch(user *User, petPhoto []byte) (*PetSearch, error) {
    ctx, err := i.ctxFactory.CreateContext()
    if err != nil {
        return nil, err
    }
    defer ctx.Complete()
    pet, err := ctx.PetRepository().CreatePet(petPhoto)
    if err != nil {
        return nil, err
    }
    search, err := ctx.SearchRepository().CreateSearch(pet)
    if err != nil {
        return nil, err
    }
    if err := i.searchScheduler.Schedule(search.id); err != nil {
        return nil, err
    }
    ctx.Commit()
    return search, nil
}

Okay, this is much more testable. In our tests we can inject a fake implementation of PersistenceContextFactory which returns a fake PersistenceContext into the PetSearchInteractor. What does the implementation look like?

Implementation

type PersistenceContextImpl struct {
    tx *sql.Tx
}

func (p *PersistenceContext) PetRepository() PetRepository {
    return NewPetRepository(tx)
}

func (p *PersistenceContext) SearchRepositor() SearchRepository {
    return NewSearchRepository(tx)
}

type PersistenceContextFactoryImpl struct {
    db *sql.DB
}

func (p *PersistenceContextFactoryImpl) CreateContext() (PersistenceContext, error) {
    tx, err := p.db.Begin()
    if err != nil {
        return err
    }
    return PersistenceContextImpl{tx}
}

In this implementation we assume that the repositories all operate on a transaction, which is injected into them by the PersistenceContext. This allows us to write tests for the repositories without muddying the test code with transaction management.

Conclusion

So, there we have it, a simple pattern for writing multi repository transactions in go, I hope it helps!