Stage 7 · Master
Workspace Layout, Design Principles, and the Platform Library
Command and Internal Package Conventions
The cmd/<service>/main.go entrypoint and internal/ package layout applied identically across all four services.
The Convention
Every service in Meridian follows the same two-part layout: a cmd/<service>/main.go entrypoint, and an internal/ directory holding packages private to that service's module. This convention is applied uniformly across apps/gateway, apps/identity-service, apps/community-service, and apps/billing-service from each service's first commit, not introduced incrementally as main.go accumulates logic.
The Gateway Layout
apps/gateway/cmd/gateway/main.gocreateResponsibility: Process entrypoint: reads config, builds the router and middleware chain, starts the HTTP server.
Why now: cmd/<service>/ rather than a bare main.go at the module root keeps the entrypoint consistent if a module ever needs a second binary (for example, a one-off migration tool).
apps/gateway/internal/config/config.gocreateResponsibility: Holds Port, LogLevel, and the three downstream service base URLs (IdentityServiceURL, CommunityServiceURL, BillingServiceURL).
Why now: Configuration is read once at startup and passed explicitly to whatever needs it — no package reaches into environment variables on its own.
apps/gateway/internal/router/router.gocreateResponsibility: Builds the http.NewServeMux() and registers routes. Currently registers only /healthz; reverse-proxy routes to the three internal services are planned but not yet implemented.
Why now: Isolating route registration in its own package keeps main.go limited to wiring, and gives the reverse-proxy routes planned for Chapter 6 a single place to be added.
apps/gateway/internal/middleware/middleware.gocreateResponsibility: Implements WithRecover, a panic-recovery middleware wrapping the handler chain.
Why now: A panic in one request must not crash the process for every other in-flight request. This is the only middleware implemented so far; auth and rate-limiting middleware are added in later chapters when those features are built.
internal/ as a Compiler-Enforced Boundary
A package rooted under internal/ can only be imported by code within the same parent directory tree. This is enforced by the Go compiler, not by convention alone. Placing config, router, middleware, and domain packages under apps/<service>/internal/ guarantees no other service module — and no external consumer of the workspace — can import gateway-internal or identity-service-internal packages directly.
What Differs Per Service
The three business-domain services — identity-service, community-service, billing-service — each additionally have an internal/domain/domain.go file, currently empty. This is intentional: no entities, interfaces, or repositories are defined until the implementation phase for that service's business logic begins. The gateway has no domain package, since it owns no persistent data of its own — its only responsibilities are routing and cross-cutting concerns.
The Decision
- Every service follows cmd/<service>/main.go plus internal/{config,router,middleware}, uniformly, from its first commit.
- Business-domain services additionally have internal/domain/domain.go, left empty until the entities it defines are actually implemented.
- internal/ is a compiler-enforced boundary: no cross-service or cross-module import of another service's internal packages is possible.
- New internal/ packages (e.g. internal/repository, internal/handler) are added within a service only when that service's next feature requires them — the outer cmd/internal shape is fixed; what's inside internal/ grows incrementally per service.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.