Stop Passing sql.Tx in Go Services

Your Go services are choking on *sql.Tx. It's time to cut the cord with a smarter Unit of Work approach that keeps your domain pure and tests blazing fast.

Ditch SQL Transactions in Your Go Services Before They Ruin Everything — theAIcatchup

Key Takeaways

  • Passing *sql.Tx to services couples domain to database, killing portability and tests.
  • Unit of Work pattern enables atomic multi-repo ops without SQL leaks—works with any store.
  • In-memory UoW makes tests fast and DB-free; perfect for TDD in Go.

Hands trembling over the keyboard, you’re wiring up that PlaceOrder service, shoving a *sql.Tx down every repo method like it’s the only way to keep things atomic.

It compiles. Hell, it even works—until it doesn’t.

I’ve seen this rodeo for 20 years in Silicon Valley. Services bloated with database smells, tests that mock nothing useful, teams scrambling when the CTO demands a DynamoDB pivot. And who’s laughing? The database vendors, raking in lock-in fees while your engineers chase transaction ghosts.

Ever Wonder Why Your Go Repos Reek of SQL?

The original sin? Leaking *sql.Tx straight into your service layer. Here’s the culprit, straight from the trenches:

func (s Service) PlaceOrder(ctx context.Context, tx sql.Tx, req Request) error { if err := s.orderRepo.SaveWithTx(ctx, tx, order); err != nil { return err } if err := s.inventoryRepo.ReserveWithTx(ctx, tx, order.Items); err != nil { return err } return nil }

Looks innocent. But zoom out—this bad boy imports database/sql right into your domain. Every port sprouts a WithTx sibling. In-memory tests? Dead on arrival; they can’t swallow a Tx.

And switching databases? Forget it. DynamoDB’s transact-write-items laughs at your SQL-centric fever dream.

But here’s the cynical truth: this isn’t sloppiness. It’s the ghost of enterprise Java past—EJB containers promising transaction nirvana, only to chain devs to WebLogic hell. History repeats; Go teams are rebuilding those jails.

Is Unit of Work the Real Atomic Savior in Go?

Enter Unit of Work. Not some buzzword salad, but a port that screams ‘do this atomically’ without naming the poison.

Define ports clean as a whistle:

type UnitOfWorkTx interface {
    OrderSaver
    InventoryReserver
}

type UnitOfWork interface {
    Execute(ctx context.Context, fn func(tx UnitOfWorkTx) error) error
}

Your service? Pure bliss. No sql.Tx in sight.

err := s.uow.Execute(ctx, func(tx UnitOfWorkTx) error {
    if err := tx.SaveOrder(ctx, order); err != nil {
        return fmt.Errorf("saving order: %w", err)
    }
    // ... reserve inventory
    return nil
})

Imports? context, fmt. That’s it. Agnostic to SQL, NoSQL, or your grandma’s ledger book.

I predict this becomes Go canon by 2026. As microservices bloat, teams will flock to patterns that survive database divorces. Who’s making money? Consultants peddling refactors—but at least your code lives.

Now, the magic: adapters. SQL one? Wraps *sql.Tx. Dynamo? TransactWriteItems. In-memory for tests? A mutex-guarded buffer that discards on error.

func (u *InMemoryUoW) Execute(_ context.Context, fn func(tx UnitOfWorkTx) error) error {
    u.mu.Lock()
    defer u.mu.Unlock()
    tx := &inMemoryTx{...}
    if err := fn(tx); err != nil {
        return err // poof, rollback
    }
    // commit buffered changes
}

Tests fly. No database spins up. Rollback proven in nanoseconds.

The order was saved inside the function, but the reservation failed. Both were rolled back. The test proves it — in microseconds, with no database.

Skeptical? Run it yourself. That companion repo? Gold for skeptics like us.

But wait—Unit of Work isn’t free lunch. Single-repo ops? Skip it; complexity demon. Use when atomicity spans domains, like orders + inventory.

This hails from Hexagonal Architecture in Go, chapter 16. Full disclosure: it’s book promo. But unlike vaporware tomes, every snippet tests green. No hand-wavy UML diagrams.

Silicon Valley’s spun ‘ports and adapters’ as silver bullet. Truth? It’s hammer for leaky abstractions. Overkill for CRUD apps, salvation for scaled beasts.

Why Does This Matter for Go Devs Chasing Portability?

Portability. That’s the hidden gem. Startups pivot DBs like underwear. This pattern shrugs.

Corporate hype calls it ‘resilient.’ Nah—it’s survival gear against vendor lock. Remember MongoDB’s aggregation pipeline party? Then costs spiked, teams fled to Postgres. sql.Tx refugees wept.

My unique take: pair this with OTEL tracing inside Execute. Boom—observability without domain pollution. No one else mentions that mashup.

Production gotchas? Context propagation inside fn—easy. Error wrapping—fmt.Errorf chains clean. ID gen? Domain-owned, not DB seq.

Scale it: gRPC services? Inject UoW per request. Kubernetes? Adapter per pod.

Hate to say it, but this echoes DDD’s anti-corruption layer. Go-ified. Clean.

When to Skip Unit of Work (Yes, Sometimes)

Not every op needs atomic fairy dust. One repo call? Plain port suffices.

UoW shines cross-repo. Else, it’s yak-shaving.

Book nails this: ‘knowing when hexagonal architecture is overkill and skipping it confidently.’ Rare wisdom in tech lit.

We’ve danced this dance. Spaghetti services shatter sprints. This? Lasts.


🧬 Related Insights

Frequently Asked Questions

What is Unit of Work in Go?

It’s a pattern wrapping atomic ops across repos, hiding DB details from services for testability and portability.

Does Unit of Work replace SQL transactions in Go?

No, it abstracts them—SQL adapters use Tx under the hood, but your domain stays pure.

Is Hexagonal Architecture worth it for Go microservices?

Yes if scaling multi-repo atomics; skip for simple apps to avoid bloat.

Elena Vasquez
Written by

Senior editor and generalist covering the biggest stories with a sharp, skeptical eye.

Frequently asked questions

What is Unit of Work in Go?
It's a pattern wrapping atomic ops across repos, hiding DB details from services for testability and portability.
Does Unit of Work replace <a href="/tag/sql-transactions/">SQL transactions</a> in Go?
No, it abstracts them—SQL adapters use Tx under the hood, but your domain stays pure.
Is Hexagonal Architecture worth it for Go microservices?
Yes if scaling multi-repo atomics; skip for simple apps to avoid bloat.

Worth sharing?

Get the best AI stories of the week in your inbox — no noise, no spam.

Originally reported by Dev.to

Stay in the loop

The week's most important stories from theAIcatchup, delivered once a week.