Retour au blog

Structuring a Go Modular Monolith

How I organize Go services as modular monoliths — clear internal boundaries, shared infrastructure, and a path to extraction if needed.

Most early-stage projects don’t need microservices. What they need is a codebase that doesn’t turn into a mess when it grows. A modular monolith hits that sweet spot: a single deployable binary with clear internal boundaries that make future extraction possible without rewriting everything.

The Core Idea

The rule is simple: modules communicate through explicit interfaces, never through direct package imports that cross boundaries. A billing module can use a users module’s interface, but it cannot reach into users/internal/ and call functions directly. This enforces the same discipline as a service boundary without the operational overhead.

internal/
  billing/
    handler.go
    service.go
    repo.go
    module.go      ← wires the module together
  users/
    handler.go
    service.go
    repo.go
    module.go
  shared/
    db/
    middleware/
    config/

Shared Infrastructure

The key insight is that shared infrastructure — the database pool, the config, the HTTP router — lives outside all modules. Each module gets what it needs injected at startup, not imported globally. This makes testing straightforward: you can give a module a test database without touching anything else.

The module.go Pattern

Each module exposes a New(deps) *Module constructor and registers its own routes on the shared router. main.go becomes almost trivial — it initializes shared infrastructure, constructs each module, and starts the server.

This is the pattern I used in Leviosa and Cluo. When one of those projects eventually needs a specific module to scale independently, the boundary is already there — extracting it into a separate service is a matter of swapping the in-process interface call for an HTTP or gRPC call.