go 24 lines · 1 tab

sqlc transaction wrapper that keeps call sites clean

Leah Thompson Jan 2026
1 tab
package store

import (
  "context"
  "database/sql"
)

type Querier interface {
  WithTx(tx *sql.Tx) Querier
  // ... add the methods you actually use
}

func InTx(ctx context.Context, db *sql.DB, q Querier, fn func(context.Context, Querier) error) error {
  tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
  if err != nil {
    return err
  }
  defer tx.Rollback()

  if err := fn(ctx, q.WithTx(tx)); err != nil {
    return err
  }
  return tx.Commit()
}
1 file · go Explain with highlit

When using sqlc, the generated query set usually has a WithTx method. I wrap that pattern so business logic can depend on an interface and still run inside a transaction. The key is to keep transaction boundaries explicit while avoiding passing *sql.Tx everywhere. The helper below begins a transaction, creates a transaction-bound querier via WithTx, runs the function, and commits on success. defer tx.Rollback() is the boring safety net that prevents leaked transactions on early returns. In production, I also set an isolation level via sql.TxOptions and I ensure ctx has a deadline so transactions don’t sit open. This is a small abstraction, but it keeps service methods tidy and makes unit testing easier because you can mock the interface.


Related snips

Share this code

Here's the card — post it anywhere.

sqlc transaction wrapper that keeps call sites clean — share card
Link copied