Module Local Handler Organization

Overview

In our Hexagonal Architecture, handlers (Driving Adapters) are organized per module. This ensures that a domain-bounded context (module) contains its own entry points, whether they are HTTP requests, NATS events, or future protocols like gRPC.

Directory Structure

Modules follow a strict internal structure where all adapters reside in the adapter/ directory, split into inbound (Driving) and outbound (Driven).
internal/modules/
└── leave/
    ├── domain/                  # Hexagon Core: Pure Business Logic
    ├── application/             # Hexagon Core: Use Cases & Ports
    │   ├── port/                # Interface definitions
    │   └── usecase/             # Business logic implementations
    ├── adapter/                 # Adapters layer
    │   ├── inbound/             # Driving Adapters (Input)
    │   │   ├── http/            # REST API Handlers
    │   │   │   ├── handler.go
    │   │   │   └── approval_handler.go
    │   │   └── event/           # NATS/Message Bus Consumers
    │   │       └── consumer.go
    │   └── outbound/            # Driven Adapters (Output)
    │       └── persistence/     # DB implementations
    │           ├── mongodb/
    │           └── postgresql/
    └── module.go                # Module wiring & dependency injection

Pattern Explanation

✅ Correct Pattern (Current)

Handlers are part of the module’s adapter layer:
  • internal/modules/leave/adapter/inbound/http/handler.go
  • internal/modules/leave/adapter/inbound/event/consumer.go

❌ Incorrect Pattern (Avoided)

Grouping all handlers for all modules in a single global directory:
  • internal/adapters/handler/http/leave_handler.go
  • internal/adapters/handler/http/user_handler.go

Benefits of Module-Local Handlers

  1. Encapsulation: Everything related to the “Leave” domain is in the leave/ directory.
  2. Ease of Deletion: To remove a feature, you simply delete the module’s directory. No need to hunt for handlers in global folders.
  3. Independence: Modules can choose different adapter structures if necessary without affecting others.
  4. Local Context: Handlers have direct access to module-local types and constants.

Module Wiring

Each module’s module.go is responsible for initializing its own adapters and registering its routes.
package leave

import (
    "github.com/waynecheah/go-core/internal/modules/leave/adapter/inbound/http"
    "github.com/waynecheah/go-core/internal/modules/leave/application/usecase"
)

type Module struct {
    Service port.LeaveService
    handler *http.LeaveHandler
}

func NewModule(c *app.Container) *Module {
    // 1. Initialize Usecases
    service := usecase.NewLeaveService(...)

    // 2. Initialize Adapters (Handlers)
    handler := http.NewLeaveHandler(service)

    return &Module{
        Service: service,
        handler: handler,
    }
}

func (m *Module) RegisterRoutes(e *echo.Group) {
    // Handlers register their own routes under the provided group
    m.handler.RegisterRoutes(e)
}

Future Protocol Support

When adding support for a new protocol (e.g., GraphQL or gRPC), follow the same pattern:
  1. Create internal/modules/[module]/adapter/inbound/graphql/
  2. Implement the resolver/handler there.
  3. Inject the new adapter in module.go.
This keeps the module’s external interfaces clearly separated from its internal logic while remaining easy to discover.