Core Concepts
Transactions

Transactions

go-lightning works seamlessly with database transactions through the Executor interface.

The Executor Interface

All go-lightning functions accept an Executor:

type Executor interface {
    Exec(query string, args ...any) (sql.Result, error)
    Query(query string, args ...any) (*sql.Rows, error)
    QueryRow(query string, args ...any) *sql.Row
}

Both *sql.DB and *sql.Tx implement this interface, so you can use the same code with or without transactions.

Basic Transaction Pattern

func CreateUserWithProfile(db *sql.DB, user *User, profile *Profile) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback() // No-op if committed
 
    // Insert user
    userId, err := lit.Insert(tx, user)
    if err != nil {
        return err
    }
 
    // Insert profile with user ID
    profile.UserId = userId
    _, err = lit.Insert(tx, profile)
    if err != nil {
        return err
    }
 
    return tx.Commit()
}

Repository Pattern

A common pattern is to accept an Executor in your repository methods:

type UserRepository struct{}
 
func (r *UserRepository) Create(ex lit.Executor, user *User) (int, error) {
    return lit.Insert(ex, user)
}
 
func (r *UserRepository) FindById(ex lit.Executor, id int) (*User, error) {
    return lit.SelectSingle[User](ex,
        "SELECT id, first_name, last_name, email FROM users WHERE id = $1", id)
}
 
func (r *UserRepository) Update(ex lit.Executor, user *User) error {
    return lit.Update(ex, user, "id = $1", user.Id)
}
 
func (r *UserRepository) Delete(ex lit.Executor, id int) error {
    return lit.Delete(ex, "DELETE FROM users WHERE id = $1", id)
}

Usage Without Transaction

repo := &UserRepository{}
 
user, err := repo.FindById(db, 123)

Usage With Transaction

repo := &UserRepository{}
 
tx, err := db.Begin()
if err != nil {
    return err
}
defer tx.Rollback()
 
user, err := repo.FindById(tx, 123)
if err != nil {
    return err
}
 
user.Email = "new@example.com"
if err := repo.Update(tx, user); err != nil {
    return err
}
 
return tx.Commit()

Complete Example

type OrderService struct {
    db        *sql.DB
    users     *UserRepository
    orders    *OrderRepository
    inventory *InventoryRepository
}
 
func (s *OrderService) PlaceOrder(userId int, items []OrderItem) error {
    tx, err := s.db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback()
 
    // Verify user exists
    user, err := s.users.FindById(tx, userId)
    if err != nil {
        return err
    }
    if user == nil {
        return errors.New("user not found")
    }
 
    // Create order
    order := &Order{UserId: userId, Status: "pending"}
    orderId, err := s.orders.Create(tx, order)
    if err != nil {
        return err
    }
 
    // Add items and update inventory
    for _, item := range items {
        item.OrderId = orderId
        if _, err := s.orders.AddItem(tx, &item); err != nil {
            return err
        }
        if err := s.inventory.Decrement(tx, item.ProductId, item.Quantity); err != nil {
            return err
        }
    }
 
    return tx.Commit()
}

Error Handling

The deferred Rollback() pattern is safe because:

  • If Commit() succeeds, Rollback() is a no-op
  • If any error occurs before Commit(), the transaction is rolled back
  • If Commit() fails, the transaction is also rolled back
tx, err := db.Begin()
if err != nil {
    return err
}
defer tx.Rollback() // Safe: no-op after successful commit
 
// ... operations ...
 
if err := tx.Commit(); err != nil {
    // Rollback already called by defer
    return fmt.Errorf("failed to commit: %w", err)
}
return nil