Skip to content

Commit

Permalink
Added an example
Browse files Browse the repository at this point in the history
  • Loading branch information
aneshas committed Mar 15, 2024
1 parent 5883616 commit e5c7b96
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 5 deletions.
133 changes: 129 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,133 @@
[![Coverage Status](https://coveralls.io/repos/github/aneshas/tx/badge.svg)](https://coveralls.io/github/aneshas/tx)
[![Go Reference](https://pkg.go.dev/badge/github.com/aneshas/tx.svg)](https://pkg.go.dev/github.com/aneshas/tx)

Package tx provides a simple transaction abstraction in order to enable decoupling/abstraction of persistence from
application/domain logic while still leaving transaction control to the application service.
(Something like @Transactional annotation in Java, without an annotation)
`go get github.com/aneshas/tx/v2`

`go get github.com/aneshas/tx`
Package tx provides a simple abstraction which leverages `context.Context` in order to provide a transactional behavior
which one could use in their use case orchestrator (eg. application service, command handler, etc...). You might think of it
as closest thing in `Go` to `@Transactional` annotation in Java or the way you could scope a transaction in `C#`.

Many people tend to implement this pattern in one way or another (I have seen it and did it quite a few times), and
more often then not, the implementations still tend to couple your use case orchestrator with your database adapters (eg. repositories) or
on the other hand, rely to heavily on `context.Context` and end up using it as a dependency injection mechanism.

This package relies on `context.Context` in order to simply pass the database transaction down the stack in a safe and clean way which
still does not violate the reasoning behind context package - which is to carry `request scoped` data across api boundaries - which is
a database transaction in this case.

## Drivers
Library currently supports `pgx` and stdlib `sql` out of the box although it is very easy to implement any additional ones
you might need.

## Example
Let's assume we have the following very common setup of an example account service which has a dependency to account repository.

```go
type Repo interface {
Save(ctx context.Context, account Account) error
Find(ctx context.Context, id int) (*Account, error)
}

func NewAccountService(transactor tx.Transactor, repo Repo) *AccountService {
return &AccountService{
Transactor: transactor,
repo: repo,
}
}

type AccountService struct {
// Embedding Transactor interface in order to decorate the service with transactional behavior,
// although you can choose how and when you use it freely
tx.Transactor

repo Repo
}

type ProvisionAccountReq struct {
// ...
}

func (s *AccountService) ProvisionAccount(ctx context.Context, r ProvisionAccountReq) error {
return s.WithTransaction(ctx, func (ctx context.Context) error {
// ctx contains an embedded transaction and as long as
// we pass it to our repo methods, they will be able to unwrap it and use it

// eg. multiple calls to the same or different repos

return s.repo.Save(ctx, Account{
// ...
})
})
}
```

You will notice that the service looks mostly the same as it would normally apart from embedding `Transactor` interface
and wrapping the use case execution using `WithTransaction`, both of which say nothing of the way the mechanism is implemented (no infrastructure dependencies).

### Repo implementation
Then, your repo might use postgres with pgx and have the following example implementation:

```go
func NewAccountRepo(pool *pgxpool.Pool) *AccountRepo {
return &AccountRepo{
pool: pool,
}
}

type AccountRepo struct {
pool *pgxpool.Pool
}

func (r *AccountRepo) Save(ctx context.Context, account Account) error {
_, err := r.conn(ctx).Exec(ctx, "...")

return err
}

func (r *AccountRepo) Find(ctx context.Context, id int) (*Account, error) {
rows, err := r.conn(ctx).Query(ctx, "...")
if err != nil {
return nil, err
}

_ = rows

return nil, nil
}

type Conn interface {
Exec(ctx context.Context, sql string, arguments ...any) (commandTag pgconn.CommandTag, err error)
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
}

func (r *AccountRepo) conn(ctx context.Context) Conn {
if tx, ok := pgxtxv5.From(ctx); ok {
return tx
}

return r.pool
}
```

Again, you may freely choose how you implement this and whether or not you actually do use the wrapped
transaction or not.

### main
Then your main function would simply tie everything together like this for example:

```go
func main() {
var pool *pgxpool.Pool

svc := NewAccountService(
tx.New(pgxtxv5.NewDBFromPool(pool)),
NewAccountRepo(pool),
)

_ = svc
}
```

This way, your infrastructural concerns stay in the infrastructure layer where they really belong.

*Please note that this is only one way of using the abstraction*
1 change: 0 additions & 1 deletion example/transfer_money.go

This file was deleted.

0 comments on commit e5c7b96

Please sign in to comment.