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

b. Implementasi Concrete UoW

c. Repository yang Support Transaction

d. Service dengan Unit of Work

e. Setup di Main


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

b. Implementasi Transaction Manager

c. Repository yang Aware Transaction

d. Service dengan Transaction Manager


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

b. Event Implementations

c. Event Handlers

d. Transactional Event Dispatcher

e. Service dengan Domain Events


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

b. Command Implementations

c. Transaction Decorator

d. Service dengan Command Pattern


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:


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:

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