https://github.com/metalfm/transactor
Transactor is a library for simplifying transaction management in Go
https://github.com/metalfm/transactor
database go golang transaction-manager transactions
Last synced: 19 days ago
JSON representation
Transactor is a library for simplifying transaction management in Go
- Host: GitHub
- URL: https://github.com/metalfm/transactor
- Owner: metalfm
- License: mit
- Created: 2025-04-15T10:24:12.000Z (about 1 year ago)
- Default Branch: master
- Last Pushed: 2026-05-17T19:43:14.000Z (about 1 month ago)
- Last Synced: 2026-05-17T20:46:20.906Z (about 1 month ago)
- Topics: database, go, golang, transaction-manager, transactions
- Language: Go
- Homepage:
- Size: 122 KB
- Stars: 4
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-go-with-stars - transactor - safe transaction boundary abstraction with adapters for database/sql, sqlx, and pgx. | 2026-05-25 | (Data Integration Frameworks / Interfaces to Multiple Backends)
- fucking-awesome-go - transactor - Type-safe transaction boundary abstraction with adapters for database/sql, sqlx, and pgx. (Database Drivers / Interfaces to Multiple Backends)
- awesome-go - transactor - Type-safe transaction boundary abstraction with adapters for database/sql, sqlx, and pgx. (Database Drivers / Interfaces to Multiple Backends)
README
# Transactor
[](https://github.com/metalfm/transactor/actions/workflows/ci.yml)
[](https://codecov.io/gh/metalfm/transactor)
[](https://goreportcard.com/report/github.com/metalfm/transactor)
[](https://pkg.go.dev/github.com/metalfm/transactor)
`Transactor` is a library for simplifying transaction management in Go.
It provides the `Transactor[T any]` interface,
which allows performing operations within a transaction while abstracting the transaction management logic.
## Installation
Add the module to your project:
```bash
go get github.com/metalfm/transactor
```
Then import the core package and the driver implementation your project uses.
### Using `database/sql`
```go
import (
"github.com/metalfm/transactor/tr"
"github.com/metalfm/transactor/driver/sql/trm"
)
```
### Using `sqlx`
```go
import (
"github.com/metalfm/transactor/tr"
"github.com/metalfm/transactor/driver/sqlx/trm"
)
```
### Using `pgx`
```go
import (
"github.com/metalfm/transactor/tr"
"github.com/metalfm/transactor/driver/pgx/trm"
)
```
Currently, the `transactor` library supports the `database/sql` driver from Go's standard library, `sqlx`, and `pgx`.
## Key Concepts
### 1. `Transactor[T any]` Interface
The interface is simple, and `[T any]` means it can accept any type, allowing it to work with various repository
implementations while maintaining type safety at compile time.
```go
type Transactor[T any] interface {
InTx(ctx context.Context, fn func (T) error) error
}
```
The `InTx` method takes a context and a function. This function contains the logic that should be executed within the
transaction.
`T` is the type of repository that will be used in the business logic.
- If the function returns an error, the transaction is rolled back.
- If the function completes successfully, the transaction is committed.
Example usage:
```go
package example
type repoTx interface {
CreateUser(ctx context.Context, name string) error
CreateOrder(ctx context.Context, items []string) error
}
err := transactor.InTx(ctx, func (repo repoTx) error {
err := repo.CreateUser(ctx, "John Doe")
if err != nil {
return err
}
err = repo.CreateOrder(ctx, []string{"item1", "item2"})
if err != nil {
return err
}
return nil
})
```
Note that all dependencies are based on interfaces, making it easy to mock them in tests as well as specific
implementations.
### 2. Repositories and Factory Method
Repositories depend on the `trm.Query` interface, which provides methods for executing SQL queries. This interface is
part of the specific database driver implementation.
The `trm.Transaction` interface, which extends `trm.Query`, is used for transaction management and adds `Commit` and
`Rollback` methods.
#### Definition of `trm.Query` and `trm.Transaction` Interfaces
```go
package trm
import (
"context"
"database/sql"
)
// Query — interface for executing SQL queries.
type Query interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
// Transaction — interface for transaction management.
// Extends Query and adds Commit and Rollback methods.
type Transaction interface {
Query
Commit() error
Rollback() error
}
```
#### Factory Method `WithTx`
The factory method `WithTx` is used for transaction management and returns a new instance of the repository associated
with the `trm.Transaction`. This isolates transaction logic within repositories.
Example implementation of the factory method:
```go
package example
import (
"github.com/metalfm/transactor/driver/sql/trm"
)
type RepoUser struct {
q trm.Query
}
func NewRepoUser(q trm.Query) *RepoUser {
return &RepoUser{q}
}
// WithTx example of a factory method
// all methods of *RepoUser will be called within the transaction
func (slf *RepoUser) WithTx(tx trm.Transaction) *RepoUser {
return &RepoUser{q: tx}
}
```
Using the factory method allows explicit transaction passing, making the code more readable and safer. Note that the
factory method `WithTx` returns a new instance of `*RepoUser`, and duck typing avoids importing interfaces into business
logic.
Be careful when implementing `WithTx`: the library cannot verify that the returned repository actually uses the provided
transaction. Returning `nil`, returning the original repository, or returning a repository that still uses the original
database connection can lead to runtime panics or queries executed outside the transaction.
Incorrect implementation:
```go
func (slf *RepoUser) WithTx(tx trm.Transaction) *RepoUser {
return slf
}
```
Correct implementation:
```go
func (slf *RepoUser) WithTx(tx trm.Transaction) *RepoUser {
return &RepoUser{q: tx}
}
```
### 3. Adapter for Repositories
The adapter is not part of the `transactor` library but provides the ability to combine code from various repositories
using an adapter. The adapter encapsulates the logic of working with multiple repositories, providing a unified
interface for working with them, including performing operations within a single transaction.
```go
package example
import (
"github.com/metalfm/transactor/driver/sql/trm"
)
type Adapter struct {
repoUser *RepoUser
repoOrder *RepoOrder
}
func NewAdapter(repoUser *svc.RepoUser, repoOrder *svc.RepoOrder) *Adapter {
return &Adapter{
repoUser: repoUser,
repoOrder: repoOrder,
}
}
// WithTx example of a factory method for combining logic from multiple repositories
func (slf *Adapter) WithTx(tx trm.Transaction) *Adapter {
return &Adapter{
repoUser: slf.repoUser.WithTx(tx),
repoOrder: slf.repoOrder.WithTx(tx),
}
}
```
### 4. Why is the Factory Method Better Than Passing Transactions Through Context?
- **Explicitness**: Transactions are passed explicitly through the factory method, not hidden in the context, making the
code more readable and understandable.
- **Safety**: Context is intended for passing request-related data (e.g., timeouts or metadata), not for managing
transaction state.
- **Encapsulation**: The factory method isolates transaction logic within repositories, preventing it from spreading to
other parts of the code.
- **Testability**: The factory method simplifies creating mocks for testing since the transaction remains part of the
repository interface.
- **Performance**: Passing transactions through the factory method does not require additional operations, such as
extracting data from the context or type casting. This makes transaction management faster and more efficient compared
to using context.
### 5. Example Service
The `Service` contains business logic and depends only on the local `repoTx` contract and a small transactional
callback. It knows nothing about the internal structure of transactions, simplifying testing and isolating logic.
Example:
```go
package app
import (
"context"
"fmt"
"github.com/metalfm/transactor/tr"
)
// repoTx declares dependencies for business logic
// all repository methods that will be used within the transaction
type repoTx interface {
CreateUser(ctx context.Context, name string) error
CreateOrder(ctx context.Context, items []string) error
}
type inTx func(context.Context, func(repoTx) error) error
type Service struct {
inTx inTx
}
func NewService[T repoTx](tr tr.Transactor[T]) *Service {
return &Service{
inTx: func(ctx context.Context, fn func(repoTx) error) error {
return tr.InTx(ctx, func(r T) error { return fn(r) })
},
}
}
func (slf *Service) Create(ctx context.Context, name string, items []string) error {
err := slf.inTx(ctx, func(r repoTx) error {
err := r.CreateUser(ctx, name)
if err != nil {
return fmt.Errorf("create user: %w", err)
}
err = r.CreateOrder(ctx, items)
if err != nil {
return fmt.Errorf("create order: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("create user & order: %w", err)
}
return nil
}
```
You can find the full example here — [example](https://github.com/metalfm/transactor/tree/master/internal/example).
### 6. Testing and `trtest` Package
To simplify testing, the library provides the `trtest` package, which allows creating mock implementations of the
`Transactor[T any]` interface. This is useful for isolating business logic from the real database.
Example usage of
`trtest.MockTransactor` — [example](https://github.com/metalfm/transactor/blob/master/internal/example/app/service_test.go)
## Benchmarks
All benchmarks were conducted using the following setup:
- **Machine**: Apple M1 Pro (Darwin, arm64)
- **Database**: PostgreSQL running in Docker
To reproduce the benchmarks, ensure you have Docker installed and run the following commands:
```bash
make up && make bench
```
### Libraries Used in Comparison
The following libraries and approaches were used for benchmarking:
1. **Native** — a basic approach using the standard `sql.DB` driver from Go's standard library without additional
abstractions.
2. **⚡ Transactor** — the tested `Transactor` library, which provides the `Transactor[T any]` interface for transaction
management.
3. **[Avito](https://github.com/avito-tech/go-transaction-manager)** — an approach based on the transaction manager
implementation used in Avito projects.
4. **[Aneshas](https://github.com/aneshas/tx)** — an alternative library for transaction management.
5. **[Thiht](https://github.com/Thiht/transactor)** — another library for transaction management.
Each approach was tested on identical scenarios to ensure an objective comparison of performance, memory consumption,
and allocation count.
### Benchmark Results
#### Execution Time (sec/op)
| Metric | Native | ⚡ Transactor | Avito | Aneshas | Thiht |
|------------|-------------|----------------------------|---------------------------------|----------------------------|----------------------------|
| **sec/op** | 289.6µ ± 4% | 284.5µ ± 2% -1.75% (p=0.025) | 288.6µ ± 2% ~ (p=0.383) | 283.4µ ± 1% -2.14% (p=0.006) | 285.8µ ± 1% ~ (p=0.086) |
#### Memory Consumption (B/op)
| Metric | Native | ⚡ Transactor | Avito | Aneshas | Thiht |
|----------|------------|----------------------|------------------------|----------------------|-----------------------|
| **B/op** | 1.185Ki ± 3% | 1.277Ki ± 1% +7.75% | 1.823Ki ± 2% +53.81% | 1.283Ki ± 1% +8.28% | 1.312Ki ± 2% +10.71% |
#### Allocation Count (allocs/op)
| Metric | Native | ⚡ Transactor | Avito | Aneshas | Thiht |
|---------------|------------|-----------------------|-----------------------|-----------------------|-----------------------|
| **allocs/op** | 30.00 ± 3% | 33.00 ± 3% +10.00% | 46.00 ± 0% +53.33% | 34.00 ± 3% +13.33% | 34.50 ± 1% +15.00% |
### Benchmark Analysis
#### Execution Time (`sec/op`):
- **native**: 289.6µs ± 4% — baseline performance.
- **⚡ transactor**: 284.5µs ± 2% — slightly faster than native in this run (-1.75%, p=0.025).
- **avito**: 288.6µs ± 2% — close to native, statistically insignificant (p=0.383).
- **aneshas**: 283.4µs ± 1% — slightly faster than native in this run (-2.14%, p=0.006).
- **Thiht**: 285.8µs ± 1% — close to native, statistically insignificant (p=0.086).
#### Memory Consumption (`B/op`):
- **native**: 1.185 KiB ± 3% — baseline memory usage.
- **⚡ transactor**: 1.277 KiB ± 1% — a **7.75%** increase.
- **avito**: 1.823 KiB ± 2% — a **53.81%** increase.
- **aneshas**: 1.283 KiB ± 1% — an **8.28%** increase.
- **Thiht**: 1.312 KiB ± 2% — a **10.71%** increase.
#### Allocation Count (`allocs/op`):
- **native**: 30.00 ± 3% — baseline allocation count.
- **⚡ transactor**: 33.00 ± 3% — a **10.00%** increase.
- **avito**: 46.00 ± 0% — a **53.33%** increase.
- **aneshas**: 34.00 ± 3% — a **13.33%** increase.
- **Thiht**: 34.50 ± 1% — a **15.00%** increase.
### Overall Conclusion
- **native** remains the baseline for performance.
- **⚡ transactor** introduces moderate overhead in memory and allocations while keeping execution time close to native.
- **avito** has the highest memory consumption and allocation count in this benchmark.
- **aneshas** and **Thiht** show similar memory and allocation profiles, with `Thiht` consuming slightly more.
✅ **`transactor` remains an optimal choice** for projects requiring a balance between performance, memory consumption, and architectural clarity.
## Design Trade-offs
### Nested Transactions
`transactor` does not support nested transactions by design. They are not needed for most application code and introduce
additional complexity that tends to leak into business logic. In most cases, a single transaction boundary around a use
case is enough. Compose several operations inside the same `InTx` callback instead of opening another transaction inside
it.
### Isolation Levels
`transactor` intentionally does not expose transaction isolation level configuration. In most PostgreSQL-backed
applications, [advisory locks](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS) are a
simpler and faster way to coordinate concurrent business operations, and they cover the common cases without adding
isolation-level decisions to business logic.
## License
Transactor is licensed under the MIT License. See [LICENSE](https://github.com/metalfm/transactor/blob/master/LICENSE)
for more
information.