Shared Infrastructure

Overview

The backend follows a Hexagonal Architecture (Ports & Adapters) pattern where infrastructure concerns are shared across all domain modules.

Directory Structure

internal/
├── shared/                       # Shared infrastructure & kernel
│   ├── infrastructure/           # Infrastructure implementations
│   │   ├── database/             # Database ports & adapters
│   │   │   ├── transaction.go    # TransactionManager interface
│   │   │   └── postgresql/       # GORM/Postgres implementation
│   │   └── outbox/               # Transactional Outbox pattern
│   │       ├── repository.go     # OutboxRepository interface
│   │       └── postgresql/       # Postgres implementation
│   └── domain/                   # Shared domain models
│       └── outbox_event.go       # Shared event models
└── modules/                      # Domain modules
    ├── user/
    └── leave/

Shared Infrastructure Ports

1. TransactionManager (internal/shared/infrastructure/database/transaction.go)

Purpose: Provides database transaction support for atomic operations across modules.
type TransactionManager interface {
    WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error
}
Implementation: internal/shared/infrastructure/database/postgresql/transaction.go (GORM) Usage in Leave Module:
import "github.com/waynecheah/go-core/internal/shared/infrastructure/database"

type LeaveService struct {
    txManager database.TransactionManager
    // ...
}

// In CreateLeaveRequest
err = s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
    // Atomically deduct balance
    if err := s.leaveBalanceRepo.DeductBalance(txCtx, balanceID, days); err != nil {
        return err
    }

    // Create leave request (same transaction)
    if err := s.leaveRequestRepo.Create(txCtx, request); err != nil {
        return err // Auto-rollback
    }

    return nil // Commit
})

2. OutboxRepository (internal/shared/infrastructure/outbox/repository.go)

Purpose: Implements the Transactional Outbox pattern for reliable event publishing.
type OutboxRepository interface {
    Save(ctx context.Context, event *domain.OutboxEvent) error
    FindUnpublished(ctx context.Context, limit int) ([]*domain.OutboxEvent, error)
    MarkPublished(ctx context.Context, eventID string) error
    // ...
}
Implementation: internal/shared/infrastructure/outbox/postgresql/outbox_repository.go Usage Pattern (when needed in leave module):
import (
    "github.com/waynecheah/go-core/internal/shared/infrastructure/database"
    "github.com/waynecheah/go-core/internal/shared/infrastructure/outbox"
)

type LeaveService struct {
    outboxRepo outbox.OutboxRepository
    txManager  database.TransactionManager
    // ...
}

// Example: Publishing LeaveApproved event
err = s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
    // Update leave request status
    if err := s.leaveRequestRepo.Update(txCtx, request); err != nil {
        return err
    }

    // Save event to outbox (same transaction)
    event := outbox.NewOutboxEvent(request.ID, "LeaveApproved", payload)
    if err := s.outboxRepo.Save(txCtx, event); err != nil {
        return err
    }

    return nil
})

Design Principles

✅ DO: Import from internal/shared/infrastructure

import (
    "github.com/waynecheah/go-core/internal/shared/infrastructure/database"
    "github.com/waynecheah/go-core/internal/shared/infrastructure/outbox"
)

type MyService struct {
    txManager  database.TransactionManager
    outboxRepo outbox.OutboxRepository
}

❌ DON’T: Duplicate port definitions in modules

// ❌ WRONG - Don't create this in modules/leave/ports/
type TransactionManager interface {
    WithTransaction(...) error
}

✅ DO: Initialize in module.go

func NewModule(c *app.Container) *Module {
    // Shared infrastructure (Postgres implementations)
    txManager := postgresql.NewTransactionManager(c.PostgresDB)
    outboxRepo := postgresql.NewOutboxRepository(c.PostgresDB, c.Logger)

    // Module-specific repositories
    leaveRepo := repository.NewLeaveRepository(c.DB, c.Logger)

    // Service with both shared and module-specific dependencies
    leaveService := services.NewLeaveService(
        leaveRepo,
        txManager,  // Shared
        outboxRepo, // Shared
        c.Logger,
    )

    return &Module{Service: leaveService}
}

Benefits

  1. Single Source of Truth: Infrastructure interfaces defined once
  2. Consistency: All modules use the same transaction/outbox patterns
  3. Testability: Easy to mock shared infrastructure
  4. Maintainability: Changes to infrastructure affect all modules uniformly
  5. Reusability: No code duplication across modules

When to Use Shared vs Module-Specific

Shared Infrastructure (internal/shared/infrastructure/)

  • Database transactions
  • Outbox pattern
  • Caching interfaces
  • Message queue interfaces
  • Authentication/Authorization (Better Auth integration)

Module-Specific (internal/modules/{module}/ports/)

  • Domain repositories (LeaveRepository, UserRepository)
  • Domain services (LeaveService, UserService)
  • Module-specific business logic interfaces

Example: Leave Module Dependencies

// Shared from infrastructure
import (
    "github.com/waynecheah/go-core/internal/shared/infrastructure/database"
    "github.com/waynecheah/go-core/internal/shared/infrastructure/outbox"
)

// Module-specific
import "github.com/waynecheah/go-core/internal/modules/leave/application/port"

type LeaveService struct {
    // Module-specific
    leaveRequestRepo port.LeaveRequestRepository
    leaveBalanceRepo port.LeaveBalanceRepository

    // Shared infrastructure
    txManager  database.TransactionManager
    outboxRepo outbox.OutboxRepository

    logger *logger.Logger
}
This pattern ensures clean separation between domain logic (module-specific) and infrastructure concerns (shared).