# Implementing Business Logic in Use Cases with Transaction Support in Go

> Learn how to implement business logic in Go use cases with transaction support. Delegate transaction management to infrastructure for atomic operations and clean architecture.

- Repository: [manato/golang-clean-architecture](https://github.com/manakuro/golang-clean-architecture)
- Tags: how-to-guide
- Published: 2026-03-06

---

**In Clean Architecture, use cases implement business logic by delegating transaction management to an infrastructure-layer abstraction, ensuring atomic operations without coupling to specific database implementations.**

Implementing business logic in use cases with transaction support is a critical pattern for maintaining data consistency in Clean Architecture applications. This article examines how the `manakuro/golang-clean-architecture` repository implements atomic transactions while keeping business rules isolated from infrastructure concerns.

## Clean Architecture Layer Organization

The repository follows strict layer separation where transaction support spans multiple layers:

| Layer | Package | Responsibility | Key Interface / Struct |
|-------|---------|----------------|------------------------|
| **Domain** | `pkg/domain/model` | Pure data models (`User`) | `model.User` |
| **Use‑case** | `pkg/usecase/usecase` | Business logic (`User` use case) | `usecase.User` interface & `userUsecase` struct |
| **Use‑case – Repository contracts** | `pkg/usecase/repository` | Define contracts for persistence & transaction | `UserRepository`, `DBRepository` |
| **Adapter (Repository implementation)** | `pkg/adapter/repository` | GORM‑based concrete implementations | `userRepository`, `dbRepository` |
| **Infrastructure – Router / Datastore** | `pkg/infrastructure/*` | HTTP routing & DB connection | `router.NewRouter`, `datastore.NewDB` |
| **Registry** | `pkg/registry` | Wire‑up dependencies (DI) | `registry.NewRegistry` |

## The Transaction Abstraction Pattern

### Defining the Contract in the Use Case Layer

Transaction capabilities are defined as an interface in the use-case layer to maintain independence from infrastructure details. In [`pkg/usecase/repository/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/repository/db.go), the `DBRepository` interface declares the transaction contract:

```go
type DBRepository interface {
    Transaction(func(interface{}) (interface{}, error)) (interface{}, error)
}

```

This abstraction allows use cases to execute multiple repository operations atomically without knowing whether the underlying implementation uses GORM, SQLx, or another database driver.

### Implementing Transaction Logic in the Infrastructure Layer

The concrete implementation resides in [`pkg/adapter/repository/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/db.go) within the `dbRepository` struct. This implementation handles the complex transaction lifecycle including **commit**, **rollback**, and **panic recovery**:

```go
func (r *dbRepository) Transaction(txFunc func(interface{}) (interface{}, error)) (data interface{}, err error) {
    tx := r.db.Begin()
    if tx.Error != nil { return nil, tx.Error }

    defer func() {
        if p := recover(); p != nil {
            log.Print("recover")
            tx.Rollback()
            panic(p)
        } else if err != nil {
            log.Print("rollback")
            tx.Rollback()
            panic("error")
        } else {
            err = tx.Commit().Error
        }
    }()

    data, err = txFunc(tx)
    return data, err
}

```

This pattern ensures that any panic within the transaction closure triggers a rollback, while normal errors also abort the transaction before changes are persisted.

## Implementing Business Logic in Use Cases with Transaction Support

The use case layer orchestrates business rules while delegating transaction management to the `DBRepository` abstraction. In [`pkg/usecase/usecase/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/usecase/user.go), the `userUsecase` implements the `Create` method:

```go
func (uu *userUsecase) Create(u *model.User) (*model.User, error) {
    data, err := uu.dBRepository.Transaction(func(i interface{}) (interface{}, error) {
        // All DB operations inside this closure run inside a single transaction
        u, err := uu.userRepository.Create(u)

        // Additional business steps (mailing, logging, etc.) can be placed here
        // ...

        return u, err
    })
    // Cast and error handling omitted for brevity
    ...
}

```

This approach encapsulates all database operations within a transactional boundary while keeping the use case agnostic of GORM-specific transaction APIs.

## Wiring Dependencies for Transactional Use Cases

The registry pattern in [`pkg/registry/registry.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/registry/registry.go) injects the transaction-capable repository into use cases:

```go
func NewRegistry(db *gorm.DB) *registry {
    // Wire concrete repositories
    userRepo := repository.NewUserRepository(db)
    dbRepo   := repository.NewDBRepository(db)

    // Build the use‑case with transaction support
    userUC := usecase.NewUserUsecase(userRepo, dbRepo)

    return &registry{
        userUsecase: userUC,
        // other controllers …
    }
}

```

This dependency injection ensures that the use case receives both the specific `UserRepository` for data access and the generic `DBRepository` for transaction coordination.

## Handling HTTP Requests with Transactional Use Cases

Controllers remain thin by delegating to use cases. In [`pkg/adapter/controller/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/controller/user.go), the `UserController` invokes the transactional create method:

```go
func (c *UserController) CreateUser(ctx echo.Context) error {
    var u model.User
    if err := ctx.Bind(&u); err != nil {
        return ctx.JSON(http.StatusBadRequest, err)
    }

    created, err := c.usecase.Create(&u)
    if err != nil {
        return ctx.JSON(http.StatusInternalServerError, err)
    }
    return ctx.JSON(http.StatusCreated, created)
}

```

The controller handles HTTP-specific concerns (binding, status codes) while remaining completely unaware that the underlying use case executes within a database transaction.

## Testing Transactional Business Logic

Unit tests mock the transaction abstraction to verify business logic without database dependencies:

```go
func TestCreateUser_TransactionSuccess(t *testing.T) {
    // Mock repositories
    mockUserRepo := &MockUserRepository{}
    mockDBRepo   := &MockDBRepository{
        TxFunc: func(fn func(interface{}) (interface{}, error)) (interface{}, error) {
            // Directly invoke the closure without real DB
            return fn(nil)
        },
    }

    uc := usecase.NewUserUsecase(mockUserRepo, mockDBRepo)

    // Set expectations on mockUserRepo.Create(...)
    // ...

    user := &model.User{Name: "Alice", Age: "30"}
    created, err := uc.Create(user)
    require.NoError(t, err)
    require.Equal(t, "Alice", created.Name)
}

```

This testing strategy isolates business logic by simulating transaction behavior, allowing rapid feedback without requiring database infrastructure.

## Summary

- **Transaction abstraction** in [`pkg/usecase/repository/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/repository/db.go) decouples use cases from specific database implementations like GORM.
- **Panic recovery and rollback** in [`pkg/adapter/repository/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/db.go) ensures data consistency even when unexpected runtime errors occur during transaction execution.
- **Closure-based transaction execution** allows use cases in [`pkg/usecase/usecase/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/usecase/user.go) to group multiple repository operations atomically while remaining infrastructure-agnostic.
- **Dependency injection** through [`pkg/registry/registry.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/registry/registry.go) wires transaction-capable repositories into use cases without hard-coding dependencies.
- **Testability** is achieved by mocking the `DBRepository` interface, enabling unit tests to verify transactional business logic without database connections.

## Frequently Asked Questions

### How does the use case layer remain independent of GORM when handling transactions?

The use case depends only on the `DBRepository` interface defined in [`pkg/usecase/repository/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/repository/db.go), which accepts a generic function closure. The concrete GORM implementation in [`pkg/adapter/repository/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/db.go) handles the actual transaction lifecycle, keeping GORM-specific APIs out of the use case layer.

### What happens if a panic occurs inside a transaction closure?

The `dbRepository.Transaction` implementation includes a deferred recovery function that catches panics, executes `tx.Rollback()`, and then re-panics with the original value. This ensures that partial database changes are never committed when unexpected runtime errors occur.

### Can multiple repository operations be combined in a single transaction?

Yes. The closure passed to `Transaction` in [`pkg/usecase/usecase/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/usecase/user.go) can invoke multiple repository methods, perform validation, or trigger domain events. All operations within the closure execute within the same transactional boundary, committing only if the entire closure returns a nil error.

### How do you test use cases that require transaction support without a real database?

Unit tests mock the `DBRepository` interface to immediately execute the provided closure without starting a real database transaction. This approach, demonstrated in the test example, verifies that the use case correctly orchestrates repository calls while remaining fast and isolated from infrastructure concerns.