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
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:
Use Transaction Manager untuk sebagian besar use case
Keep Business Logic in Service Layer - jangan taruh di repository
Use Domain Events untuk side effects dan decoupling
Handle Rollback Explicitly - selalu rollback pada error
Test Transaction Boundaries - test both success and failure scenarios
❌ Don't:
Pass Database Connections ke service layer
Mix Transaction Logic dengan business logic
Forget to Rollback pada error conditions
Use Global Transactions - buat transaction per request
Ignore Nested Transactions - pertimbangkan savepoints
🎯 Pattern Selection Guide:
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