Member-only story
Go Database Transactions Done Right
The defer trick, the DBTX interface, and how to keep transactions clean across your repository.
It’s a Thursday afternoon. Your service has been running fine for weeks. Then Slack lights up: a user was charged but their order was never created. You dig into the logs. The payment write succeeded. The order insert failed. They were two separate database calls, and nothing held them together.
You didn’t use a transaction. And now you’re writing a one-off script to reconcile corrupted data.
This article is about making sure that never happens. Not just the basics — BEGIN, COMMIT, done — but the production-ready pattern that keeps transactions clean, testable, and properly layered across your repositories. If you've been following the Go Repository Pattern Done Right, this is the article that completes it.
The Idiom Every Go Developer Must Know
Go’s database/sql package gives you *sql.Tx via db.Begin() or db.BeginTx(). The pattern is deceptively simple, but there's one idiom that separates production code from tutorial code.
func Transfer(ctx context.Context, db *sql.DB, fromID, toID int64, amount int) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {…





