Stage 7 · Master
Workspace Layout, Design Principles, and the Platform Library
Constructor Injection and Testability
Meridian wires every running binary explicitly in main.go. There is no DI container, no reflection-driven graph, and no hidden lifecycle: config, logger, router, and future adapters are assembled with ordinary constructor calls.
main Is the Composition Root
All four Meridian binaries already follow the same assembly pattern. main.go loads a concrete Config, constructs a structured logger through libs/platform/logging.New, builds the top-level handler graph, and finally creates http.Server. This is constructor injection in its simplest Go form: dependencies are values passed explicitly at process startup. Because assembly lives at the edge, future business packages remain free of environment lookups and global singletons.
Explicit Wiring in the Current Code
The current services are small, but the pattern already scales. gateway receives a logger because panic recovery needs it. The three internal services build a ServeMux directly because they only expose /healthz today. As real handlers arrive, constructor functions can expand one step at a time: NewTenantResolver(cfg, logger), NewInvoiceHandler(service, logger), NewReverseProxy(targetURL, logger). The composition root stays in main.go even as the graph grows.
apps/gateway/internal/proxy/proxy.gocreateResponsibility: Expose a constructor that accepts downstream base URL, logger, and future middleware dependencies explicitly.
Why now: Reverse proxy behavior should be assembled from concrete values rather than discovered through globals.
Connects to: Called by apps/gateway/cmd/gateway/main.go once proxy routes are introduced.
apps/identity-service/internal/http/handlers.gocreateResponsibility: Accept application services through constructors instead of importing storage or auth packages directly.
Why now: Handler tests can then provide narrow doubles without booting the whole process.
Connects to: Will sit above the future identity-service application and domain packages.
Why No DI Framework
| Option | Benefit | Cost |
|---|---|---|
| Reflection or container-based DI | Can hide repetitive wiring in large object graphs | Introduces indirection, startup magic, and harder-to-audit lifecycles |
| Plain constructors in main.go | Simple, explicit, compile-time visible assembly | Chosen. A little repetition is cheaper than hidden runtime wiring in the current repo scale. |
The repo deliberately prefers explicitness over automation here. Constructor injection is not being used to mimic a framework; it is being used to keep side effects obvious. A future billing-service payment client should be visible where it is created. A future gateway tenant resolver should be visible where it receives its timeout policy and logger. This also makes failure modes earlier: if a constructor cannot build because configuration is missing, startup fails before traffic is accepted.
Testability from Constructor Seams
Constructor seams are the mechanism that later enables narrow tests. A handler constructor can accept an invoice service interface or concrete service. An application service constructor can accept a repository contract and a clock. No framework is required. The same explicit graph that makes startup legible also makes tests cheap, because each test assembles only the collaborators it needs and nothing is fetched implicitly from global state.
The Decision
Meridian keeps main.go as the composition root for every binary, uses plain constructors instead of a DI framework, and treats explicit wiring as the foundation for future testability. The current /healthz-only code already demonstrates the pattern; later features extend it rather than replacing it.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.