Cara Handle Database Transactions Di Go

Pertanyaan yang sangat bagus dan fundamental dalam Clean Architecture! Ini adalah salah satu tantangan terbesar dalam mengimplementasikan CA dengan database transactions. Mari saya jelaskan secara lengkap berbagai pendekatan dan solusi terbaiknya.


1. Masalah Utama

Scenario yang Ingin Dicapai:

// service/order_service.go
type OrderService struct {
    userRepo    repository.UserRepository
    productRepo repository.ProductRepository
    orderRepo   repository.OrderRepository
}

func (s *OrderService) CreateOrder(userID, productID int) error {
    // 1. Check user exists
    user, err := s.userRepo.FindByID(userID)
    if err != nil {
        return err
    }
    
    // 2. Check product exists and has stock
    product, err := s.productRepo.FindByID(productID)
    if err != nil {
        return err
    }
    
    if product.Stock <= 0 {
        return errors.New("product out of stock")
    }
    
    // 3. Create order
    order := &Order{
        UserID:    userID,
        ProductID: productID,
        Status:    "pending",
    }
    
    err = s.orderRepo.Create(order)
    if err != nil {
        return err
    }
    
    // 4. Update product stock
    product.Stock--
    err = s.productRepo.Update(product)
    if err != nil {
        return err
    }
    
    // 5. Update user's order count
    user.OrderCount++
    err = s.userRepo.Update(user)
    if err != nil {
        return err
    }
    
    return nil
}

Masalah:

  • Jika step 4 gagal, step 3 sudah commit (tidak atomic)

  • Tidak bisa rollback semua operasi

  • Service tidak tahu tentang database transaction

  • Tidak boleh pass *pgx.Tx ke service (melanggar CA)


2. Solusi 1: Unit of Work Pattern

Konsep:

Unit of Work (UoW) adalah pattern yang meng-track perubahan pada objects dan men-commit semuanya dalam satu transaction.

Implementasi:

a. Definisikan Unit of Work Interface

// repository/unit_of_work.go
package repository

import "context"

type UnitOfWork interface {
    // Repository access
    Users() UserRepository
    Products() ProductRepository
    Orders() OrderRepository
    
    // Transaction control
    Begin(ctx context.Context) error
    Commit() error
    Rollback() error
    
    // Complete the unit of work
    Complete(ctx context.Context) error
}

b. Implementasi Concrete UoW

// repository/pgx_unit_of_work.go
package repository

import (
    "context"
    "errors"
    
    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"
)

type PGXUnitOfWork struct {
    db     *pgxpool.Pool
    tx     pgx.Tx
    ctx    context.Context
    
    // Repositories
    userRepo    *PGXUserRepository
    productRepo *PGXProductRepository
    orderRepo   *PGXOrderRepository
}

func NewPGXUnitOfWork(db *pgxpool.Pool) *PGXUnitOfWork {
    return &PGXUnitOfWork{
        db: db,
    }
}

func (uow *PGXUnitOfWork) Begin(ctx context.Context) error {
    tx, err := uow.db.Begin(ctx)
    if err != nil {
        return err
    }
    
    uow.tx = tx
    uow.ctx = ctx
    
    // Initialize repositories with transaction
    uow.userRepo = NewPGXUserRepository(tx)
    uow.productRepo = NewPGXProductRepository(tx)
    uow.orderRepo = NewPGXOrderRepository(tx)
    
    return nil
}

func (uow *PGXUnitOfWork) Users() UserRepository {
    return uow.userRepo
}

func (uow *PGXUnitOfWork) Products() ProductRepository {
    return uow.productRepo
}

func (uow *PGXUnitOfWork) Orders() OrderRepository {
    return uow.orderRepo
}

func (uow *PGXUnitOfWork) Commit() error {
    if uow.tx == nil {
        return errors.New("no transaction in progress")
    }
    return uow.tx.Commit(uow.ctx)
}

func (uow *PGXUnitOfWork) Rollback() error {
    if uow.tx == nil {
        return errors.New("no transaction in progress")
    }
    return uow.tx.Rollback(uow.ctx)
}

func (uow *PGXUnitOfWork) Complete(ctx context.Context) error {
    // Begin transaction
    if err := uow.Begin(ctx); err != nil {
        return err
    }
    
    // Commit if no errors
    return uow.Commit()
}

c. Repository yang Support Transaction

// repository/user_repository.go
package repository

import (
    "context"
    "database/sql"
    
    "github.com/jackc/pgx/v5"
)

type UserRepository interface {
    FindByID(ctx context.Context, id int) (*User, error)
    Update(ctx context.Context, user *User) error
}

type PGXUserRepository struct {
    db pgx.Tx // Bisa pgxpool.Pool atau pgx.Tx
}

func NewPGXUserRepository(db pgx.Tx) *PGXUserRepository {
    return &PGXUserRepository{db: db}
}

func (r *PGXUserRepository) FindByID(ctx context.Context, id int) (*User, error) {
    var user User
    err := r.db.QueryRow(ctx, 
        "SELECT id, name, email, order_count FROM users WHERE id = $1", id).
        Scan(&user.ID, &user.Name, &user.Email, &user.OrderCount)
    
    if err != nil {
        return nil, err
    }
    
    return &user, nil
}

func (r *PGXUserRepository) Update(ctx context.Context, user *User) error {
    _, err := r.db.Exec(ctx,
        "UPDATE users SET name = $1, email = $2, order_count = $3 WHERE id = $4",
        user.Name, user.Email, user.OrderCount, user.ID)
    return err
}

d. Service dengan Unit of Work

// service/order_service.go
package service

import (
    "context"
    "errors"
    
    "myapp/internal/repository"
)

type OrderService struct {
    uowFactory func() repository.UnitOfWork
}

func NewOrderService(uowFactory func() repository.UnitOfWork) *OrderService {
    return &OrderService{
        uowFactory: uowFactory,
    }
}

func (s *OrderService) CreateOrder(ctx context.Context, userID, productID int) error {
    // Create Unit of Work
    uow := s.uowFactory()
    
    // Begin transaction
    if err := uow.Begin(ctx); err != nil {
        return err
    }
    
    // Ensure rollback on error
    defer func() {
        if err := recover(); err != nil {
            uow.Rollback()
        }
    }()
    
    // Get repositories from UoW
    userRepo := uow.Users()
    productRepo := uow.Products()
    orderRepo := uow.Orders()
    
    // 1. Check user exists
    user, err := userRepo.FindByID(ctx, userID)
    if err != nil {
        uow.Rollback()
        return err
    }
    
    // 2. Check product exists and has stock
    product, err := productRepo.FindByID(ctx, productID)
    if err != nil {
        uow.Rollback()
        return err
    }
    
    if product.Stock <= 0 {
        uow.Rollback()
        return errors.New("product out of stock")
    }
    
    // 3. Create order
    order := &Order{
        UserID:    userID,
        ProductID: productID,
        Status:    "pending",
    }
    
    err = orderRepo.Create(ctx, order)
    if err != nil {
        uow.Rollback()
        return err
    }
    
    // 4. Update product stock
    product.Stock--
    err = productRepo.Update(ctx, product)
    if err != nil {
        uow.Rollback()
        return err
    }
    
    // 5. Update user's order count
    user.OrderCount++
    err = userRepo.Update(ctx, user)
    if err != nil {
        uow.Rollback()
        return err
    }
    
    // 6. Commit transaction
    return uow.Commit()
}

e. Setup di Main

// main.go
func main() {
    db := database.Connect()
    logger := logger.Init()
    
    // Create Unit of Work factory
    uowFactory := func() repository.UnitOfWork {
        return repository.NewPGXUnitOfWork(db)
    }
    
    // Create services
    orderService := service.NewOrderService(uowFactory)
    
    // Create handlers
    orderHandler := handlers.NewOrderHandler(orderService, logger)
    
    // Setup routes
    mux := http.NewServeMux()
    mux.HandleFunc("POST /orders", orderHandler.CreateOrder)
    
    // Start server
    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }
    
    logger.Info("Server starting on :8080")
    server.ListenAndServe()
}

3. Solusi 2: Transaction Manager Pattern

Konsep:

Menggunakan Transaction Manager yang menghandle transaction boundaries tanpa service perlu tahu implementation details.

Implementasi:

a. Transaction Manager Interface

// transaction/manager.go
package transaction

import "context"

type Manager interface {
    // Execute operation within transaction
    Execute(ctx context.Context, fn func(ctx context.Context) error) error
}

type ContextKey string

const TransactionKey ContextKey = "transaction"

b. Implementasi Transaction Manager

// transaction/pgx_manager.go
package transaction

import (
    "context"
    "errors"
    
    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"
)

type PGXTransactionManager struct {
    db *pgxpool.Pool
}

func NewPGXTransactionManager(db *pgxpool.Pool) *PGXTransactionManager {
    return &PGXTransactionManager{db: db}
}

func (tm *PGXTransactionManager) Execute(ctx context.Context, fn func(ctx context.Context) error) error {
    // Begin transaction
    tx, err := tm.db.Begin(ctx)
    if err != nil {
        return err
    }
    
    // Create new context with transaction
    txCtx := context.WithValue(ctx, TransactionKey, tx)
    
    // Execute the function
    err = fn(txCtx)
    if err != nil {
        // Rollback on error
        if rbErr := tx.Rollback(ctx); rbErr != nil {
            return errors.Join(err, rbErr)
        }
        return err
    }
    
    // Commit on success
    return tx.Commit(ctx)
}

// Helper function to get transaction from context
func GetTransaction(ctx context.Context) (pgx.Tx, bool) {
    tx, ok := ctx.Value(TransactionKey).(pgx.Tx)
    return tx, ok
}

c. Repository yang Aware Transaction

// repository/user_repository.go
package repository

import (
    "context"
    
    "myapp/internal/transaction"
    "github.com/jackc/pgx/v5"
)

type UserRepository interface {
    FindByID(ctx context.Context, id int) (*User, error)
    Update(ctx context.Context, user *User) error
}

type PGXUserRepository struct {
    db *pgxpool.Pool
}

func NewPGXUserRepository(db *pgxpool.Pool) *PGXUserRepository {
    return &PGXUserRepository{db: db}
}

func (r *PGXUserRepository) getExecutor(ctx context.Context) interface {
    QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row
    Exec(ctx context.Context, sql string, args ...interface{}) (pgx.CommandTag, error)
} {
    // Check if there's a transaction in context
    if tx, ok := transaction.GetTransaction(ctx); ok {
        return tx
    }
    
    // Use regular connection pool
    return r.db
}

func (r *PGXUserRepository) FindByID(ctx context.Context, id int) (*User, error) {
    executor := r.getExecutor(ctx)
    
    var user User
    err := executor.QueryRow(ctx, 
        "SELECT id, name, email, order_count FROM users WHERE id = $1", id).
        Scan(&user.ID, &user.Name, &user.Email, &user.OrderCount)
    
    if err != nil {
        return nil, err
    }
    
    return &user, nil
}

func (r *PGXUserRepository) Update(ctx context.Context, user *User) error {
    executor := r.getExecutor(ctx)
    
    _, err := executor.Exec(ctx,
        "UPDATE users SET name = $1, email = $2, order_count = $3 WHERE id = $4",
        user.Name, user.Email, user.OrderCount, user.ID)
    return err
}

d. Service dengan Transaction Manager

// service/order_service.go
package service

import (
    "context"
    "errors"
    
    "myapp/internal/repository"
    "myapp/internal/transaction"
)

type OrderService struct {
    txManager   transaction.Manager
    userRepo    repository.UserRepository
    productRepo repository.ProductRepository
    orderRepo   repository.OrderRepository
}

func NewOrderService(
    txManager transaction.Manager,
    userRepo repository.UserRepository,
    productRepo repository.ProductRepository,
    orderRepo repository.OrderRepository,
) *OrderService {
    return &OrderService{
        txManager:   txManager,
        userRepo:    userRepo,
        productRepo: productRepo,
        orderRepo:   orderRepo,
    }
}

func (s *OrderService) CreateOrder(ctx context.Context, userID, productID int) error {
    // Execute within transaction
    return s.txManager.Execute(ctx, func(txCtx context.Context) error {
        // 1. Check user exists
        user, err := s.userRepo.FindByID(txCtx, userID)
        if err != nil {
            return err
        }
        
        // 2. Check product exists and has stock
        product, err := s.productRepo.FindByID(txCtx, productID)
        if err != nil {
            return err
        }
        
        if product.Stock <= 0 {
            return errors.New("product out of stock")
        }
        
        // 3. Create order
        order := &Order{
            UserID:    userID,
            ProductID: productID,
            Status:    "pending",
        }
        
        err = s.orderRepo.Create(txCtx, order)
        if err != nil {
            return err
        }
        
        // 4. Update product stock
        product.Stock--
        err = s.productRepo.Update(txCtx, product)
        if err != nil {
            return err
        }
        
        // 5. Update user's order count
        user.OrderCount++
        err = s.userRepo.Update(txCtx, user)
        if err != nil {
            return err
        }
        
        return nil
    })
}

4. Solusi 3: Domain Events Pattern

Konsep:

Menggunakan domain events untuk men-decouple operasi dan men-handle side effects dalam transaction.

Implementasi:

a. Domain Events Interface

// domain/events.go
package domain

import "context"

type Event interface {
    Name() string
}

type Handler interface {
    Handle(ctx context.Context, event Event) error
}

type Dispatcher interface {
    Dispatch(ctx context.Context, event Event) error
    Register(eventName string, handler Handler)
}

b. Event Implementations

// domain/order_events.go
package domain

type OrderCreatedEvent struct {
    OrderID   int
    UserID    int
    ProductID int
}

func (e OrderCreatedEvent) Name() string {
    return "order.created"
}

type ProductStockUpdatedEvent struct {
    ProductID int
    NewStock  int
}

func (e ProductStockUpdatedEvent) Name() string {
    return "product.stock_updated"
}

type UserOrderCountUpdatedEvent struct {
    UserID        int
    NewOrderCount int
}

func (e UserOrderCountUpdatedEvent) Name() string {
    return "user.order_count_updated"
}

c. Event Handlers

// domain/handlers.go
package domain

import (
    "context"
    
    "myapp/internal/repository"
)

type ProductStockHandler struct {
    productRepo repository.ProductRepository
}

func NewProductStockHandler(productRepo repository.ProductRepository) *ProductStockHandler {
    return &ProductStockHandler{productRepo: productRepo}
}

func (h *ProductStockHandler) Handle(ctx context.Context, event Event) error {
    if e, ok := event.(ProductStockUpdatedEvent); ok {
        product, err := h.productRepo.FindByID(ctx, e.ProductID)
        if err != nil {
            return err
        }
        
        product.Stock = e.NewStock
        return h.productRepo.Update(ctx, product)
    }
    
    return nil
}

type UserOrderCountHandler struct {
    userRepo repository.UserRepository
}

func NewUserOrderCountHandler(userRepo repository.UserRepository) *UserOrderCountHandler {
    return &UserOrderCountHandler{userRepo: userRepo}
}

func (h *UserOrderCountHandler) Handle(ctx context.Context, event Event) error {
    if e, ok := event.(UserOrderCountUpdatedEvent); ok {
        user, err := h.userRepo.FindByID(ctx, e.UserID)
        if err != nil {
            return err
        }
        
        user.OrderCount = e.NewOrderCount
        return h.userRepo.Update(ctx, user)
    }
    
    return nil
}

d. Transactional Event Dispatcher

// domain/dispatcher.go
package domain

import (
    "context"
    "sync"
    
    "myapp/internal/transaction"
)

type TransactionalDispatcher struct {
    handlers    map[string][]Handler
    txManager   transaction.Manager
    pending     []Event
    mu          sync.Mutex
}

func NewTransactionalDispatcher(txManager transaction.Manager) *TransactionalDispatcher {
    return &TransactionalDispatcher{
        handlers:  make(map[string][]Handler),
        txManager: txManager,
    }
}

func (d *TransactionalDispatcher) Register(eventName string, handler Handler) {
    d.handlers[eventName] = append(d.handlers[eventName], handler)
}

func (d *TransactionalDispatcher) Dispatch(ctx context.Context, event Event) error {
    d.mu.Lock()
    d.pending = append(d.pending, event)
    d.mu.Unlock()
    return nil
}

func (d *TransactionalDispatcher) Commit(ctx context.Context) error {
    d.mu.Lock()
    events := d.pending
    d.pending = nil
    d.mu.Unlock()
    
    // Execute all handlers within the same transaction
    return d.txManager.Execute(ctx, func(txCtx context.Context) error {
        for _, event := range events {
            handlers := d.handlers[event.Name()]
            for _, handler := range handlers {
                if err := handler.Handle(txCtx, event); err != nil {
                    return err
                }
            }
        }
        return nil
    })
}

e. Service dengan Domain Events

// service/order_service.go
package service

import (
    "context"
    "errors"
    
    "myapp/internal/domain"
    "myapp/internal/repository"
)

type OrderService struct {
    orderRepo   repository.OrderRepository
    productRepo repository.ProductRepository
    dispatcher  domain.Dispatcher
}

func NewOrderService(
    orderRepo repository.OrderRepository,
    productRepo repository.ProductRepository,
    dispatcher domain.Dispatcher,
) *OrderService {
    return &OrderService{
        orderRepo:   orderRepo,
        productRepo: productRepo,
        dispatcher:  dispatcher,
    }
}

func (s *OrderService) CreateOrder(ctx context.Context, userID, productID int) error {
    // 1. Check product exists and has stock
    product, err := s.productRepo.FindByID(ctx, productID)
    if err != nil {
        return err
    }
    
    if product.Stock <= 0 {
        return errors.New("product out of stock")
    }
    
    // 2. Create order
    order := &Order{
        UserID:    userID,
        ProductID: productID,
        Status:    "pending",
    }
    
    err = s.orderRepo.Create(ctx, order)
    if err != nil {
        return err
    }
    
    // 3. Dispatch domain events
    err = s.dispatcher.Dispatch(ctx, domain.OrderCreatedEvent{
        OrderID:   order.ID,
        UserID:    userID,
        ProductID: productID,
    })
    if err != nil {
        return err
    }
    
    err = s.dispatcher.Dispatch(ctx, domain.ProductStockUpdatedEvent{
        ProductID: productID,
        NewStock:  product.Stock - 1,
    })
    if err != nil {
        return err
    }
    
    err = s.dispatcher.Dispatch(ctx, domain.UserOrderCountUpdatedEvent{
        UserID:        userID,
        NewOrderCount: 0, // Will be calculated in handler
    })
    if err != nil {
        return err
    }
    
    // 4. Commit all events
    return s.dispatcher.Commit(ctx)
}

5. Solusi 4: Command Pattern with Transaction Decorator

Konsep:

Menggunakan command pattern untuk encapsulate business logic dan decorators untuk cross-cutting concerns seperti transactions.

Implementasi:

a. Command Interface

// command/command.go
package command

import "context"

type Command interface {
    Execute(ctx context.Context) error
}

type Handler interface {
    Handle(ctx context.Context, cmd Command) error
}

b. Command Implementations

// command/order_commands.go
package command

import (
    "context"
    "errors"
    
    "myapp/internal/repository"
)

type CreateOrderCommand struct {
    UserID    int
    ProductID int
}

type CreateOrderHandler struct {
    userRepo    repository.UserRepository
    productRepo repository.ProductRepository
    orderRepo   repository.OrderRepository
}

func NewCreateOrderHandler(
    userRepo repository.UserRepository,
    productRepo repository.ProductRepository,
    orderRepo repository.OrderRepository,
) *CreateOrderHandler {
    return &CreateOrderHandler{
        userRepo:    userRepo,
        productRepo: productRepo,
        orderRepo:   orderRepo,
    }
}

func (h *CreateOrderHandler) Handle(ctx context.Context, cmd Command) error {
    createCmd, ok := cmd.(*CreateOrderCommand)
    if !ok {
        return errors.New("invalid command type")
    }
    
    // 1. Check user exists
    user, err := h.userRepo.FindByID(ctx, createCmd.UserID)
    if err != nil {
        return err
    }
    
    // 2. Check product exists and has stock
    product, err := h.productRepo.FindByID(ctx, createCmd.ProductID)
    if err != nil {
        return err
    }
    
    if product.Stock <= 0 {
        return errors.New("product out of stock")
    }
    
    // 3. Create order
    order := &Order{
        UserID:    createCmd.UserID,
        ProductID: createCmd.ProductID,
        Status:    "pending",
    }
    
    err = h.orderRepo.Create(ctx, order)
    if err != nil {
        return err
    }
    
    // 4. Update product stock
    product.Stock--
    err = h.productRepo.Update(ctx, product)
    if err != nil {
        return err
    }
    
    // 5. Update user's order count
    user.OrderCount++
    err = h.userRepo.Update(ctx, user)
    if err != nil {
        return err
    }
    
    return nil
}

c. Transaction Decorator

// command/transaction_decorator.go
package command

import (
    "context"
    
    "myapp/internal/transaction"
)

type TransactionDecorator struct {
    txManager transaction.Manager
    handler   Handler
}

func NewTransactionDecorator(txManager transaction.Manager, handler Handler) *TransactionDecorator {
    return &TransactionDecorator{
        txManager: txManager,
        handler:   handler,
    }
}

func (d *TransactionDecorator) Handle(ctx context.Context, cmd Command) error {
    return d.txManager.Execute(ctx, func(txCtx context.Context) error {
        return d.handler.Handle(txCtx, cmd)
    })
}

d. Service dengan Command Pattern

// service/order_service.go
package service

import (
    "context"
    
    "myapp/internal/command"
    "myapp/internal/transaction"
)

type OrderService struct {
    commandHandler command.Handler
}

func NewOrderService(txManager transaction.Manager) *OrderService {
    // Create base handler
    baseHandler := command.NewCreateOrderHandler(
        // Inject repositories here
    )
    
    // Wrap with transaction decorator
    transactionHandler := command.NewTransactionDecorator(txManager, baseHandler)
    
    return &OrderService{
        commandHandler: transactionHandler,
    }
}

func (s *OrderService) CreateOrder(ctx context.Context, userID, productID int) error {
    cmd := &command.CreateOrderCommand{
        UserID:    userID,
        ProductID: productID,
    }
    
    return s.commandHandler.Handle(ctx, cmd)
}

6. Perbandingan Solusi

Solusi
Keuntungan
Kerugian
Use Case

Unit of Work

- Explicit transaction control - Centralized transaction logic - Easy to understand

- More boilerplate - Tight coupling between repositories

Complex business logic with multiple repository calls

Transaction Manager

- Clean separation - Easy to test - Flexible

- Context pollution - Hidden transaction logic

Medium complexity, good balance

Domain Events

- Highly decoupled - Extensible - Good for CQRS

- Complex to implement - Eventual consistency concerns

Complex domains with many side effects

Command + Decorator

- Very clean - Easy to extend - Good for cross-cutting concerns

- Overhead for simple cases - Learning curve

Applications with many cross-cutting concerns


7. Rekomendasi: Hybrid Approach

Best Practice untuk Kebanyakan Aplikasi:

// service/order_service.go
package service

import (
    "context"
    "errors"
    
    "myapp/internal/domain"
    "myapp/internal/repository"
    "myapp/internal/transaction"
)

type OrderService struct {
    txManager   transaction.Manager
    userRepo    repository.UserRepository
    productRepo repository.ProductRepository
    orderRepo   repository.OrderRepository
    dispatcher  domain.Dispatcher
}

func (s *OrderService) CreateOrder(ctx context.Context, userID, productID int) error {
    return s.txManager.Execute(ctx, func(txCtx context.Context) error {
        // Core business logic
        if err := s.validateOrder(txCtx, userID, productID); err != nil {
            return err
        }
        
        order, err := s.createOrderEntity(txCtx, userID, productID)
        if err != nil {
            return err
        }
        
        if err := s.updateProductStock(txCtx, productID); err != nil {
            return err
        }
        
        if err := s.updateUserOrderCount(txCtx, userID); err != nil {
            return err
        }
        
        // Dispatch domain events for side effects
        return s.dispatcher.Dispatch(txCtx, domain.OrderCreatedEvent{
            OrderID:   order.ID,
            UserID:    userID,
            ProductID: productID,
        })
    })
}

func (s *OrderService) validateOrder(ctx context.Context, userID, productID int) error {
    // Validation logic
    return nil
}

func (s *OrderService) createOrderEntity(ctx context.Context, userID, productID int) (*Order, error) {
    // Create order logic
    return &Order{}, nil
}

func (s *OrderService) updateProductStock(ctx context.Context, productID int) error {
    // Update stock logic
    return nil
}

func (s *OrderService) updateUserOrderCount(ctx context.Context, userID int) error {
    // Update user logic
    return nil
}

8. Best Practices Summary

✅ Do:

  1. Use Transaction Manager untuk sebagian besar use case

  2. Keep Business Logic in Service Layer - jangan taruh di repository

  3. Use Domain Events untuk side effects dan decoupling

  4. Handle Rollback Explicitly - selalu rollback pada error

  5. Test Transaction Boundaries - test both success and failure scenarios

❌ Don't:

  1. Pass Database Connections ke service layer

  2. Mix Transaction Logic dengan business logic

  3. Forget to Rollback pada error conditions

  4. Use Global Transactions - buat transaction per request

  5. Ignore Nested Transactions - pertimbangkan savepoints

🎯 Pattern Selection Guide:

Scenario
Recommended Pattern

Simple CRUD

Transaction Manager

Complex Business Logic

Unit of Work

Event-Driven Architecture

Domain Events

Cross-Cutting Concerns

Command + Decorator

Microservices

Transaction Manager + Domain Events


9. Final Implementation Example

Complete Transaction Handling:

// main.go
func main() {
    db := database.Connect()
    logger := logger.Init()
    
    // Setup transaction manager
    txManager := transaction.NewPGXTransactionManager(db)
    
    // Setup repositories
    userRepo := repository.NewPGXUserRepository(db)
    productRepo := repository.NewPGXProductRepository(db)
    orderRepo := repository.NewPGXOrderRepository(db)
    
    // Setup domain event dispatcher
    dispatcher := domain.NewTransactionalDispatcher(txManager)
    
    // Register event handlers
    dispatcher.Register("order.created", handlers.NewOrderCreatedHandler())
    dispatcher.Register("product.stock_updated", handlers.NewProductStockHandler(productRepo))
    dispatcher.Register("user.order_count_updated", handlers.NewUserOrderCountHandler(userRepo))
    
    // Setup services
    orderService := service.NewOrderService(
        txManager,
        userRepo,
        productRepo,
        orderRepo,
        dispatcher,
    )
    
    // Setup handlers
    orderHandler := handlers.NewOrderHandler(orderService, logger)
    
    // Setup routes
    mux := http.NewServeMux()
    mux.HandleFunc("POST /orders", orderHandler.CreateOrder)
    
    // Start server
    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }
    
    logger.Info("Server starting on :8080")
    server.ListenAndServe()
}

Dengan pendekatan ini, Anda mendapatkan:

  • Clean Architecture - service tidak tahu implementation details

  • Proper Transaction Management - semua operasi atomic

  • Testability - mudah mock dan test

  • Maintainability - code yang terorganisir dan scalable

  • Flexibility - mudah extend dengan pattern lain

Ini adalah solusi yang paling robust dan scalable untuk kebanyakan aplikasi Go dengan Clean Architecture.

Last updated