Stage 7 · Master
Workspace Layout, Design Principles, and the Platform Library
Interfaces and Dependency Inversion
Meridian applies dependency inversion sparingly: the scaffolding phase keeps domain packages empty on purpose, and interfaces appear only when a concrete use case genuinely needs a second implementation or a test double.
The Scaffolding Convention
The strongest clue in the current repository is negative space. apps/identity-service/internal/domain/domain.go, apps/community-service/internal/domain/domain.go, and apps/billing-service/internal/domain/domain.go are present, named, and intentionally empty. The convention is explicit in their comments: no entities, interfaces, or repositories are defined until the corresponding implementation phase begins. That is the opposite of speculative architecture; the package boundary exists, but its abstractions do not appear before the schema and use cases are real.
In Meridian's scaffolding phase, not introducing a repository or service interface is often the more disciplined choice. Dependency inversion starts when a consumer can describe a stable behavior boundary, not when a package is first created.
Why Empty Domain Packages Are Deliberate
Premature interfaces tend to capture guesses about persistence, transport, and authorization before those concerns have any pressure from real code. In a multi-tenant HOA system, that risk is especially high: membership rules, resident-unit relationships, invoice ownership, and complaint workflows all carry tenant-scoped constraints that should shape the final contracts. Freezing a repository interface before the first concrete query exists would only bake uncertainty into the type system.
| Approach | Initial feeling | Long-term effect |
|---|---|---|
| Speculative interface first | Looks architectural because every layer gets a contract immediately | Usually encodes guessed methods and guessed boundaries that later resist change |
| Concrete first, interface later when required | Feels minimal during scaffolding | Chosen. Contracts emerge from real use cases, tests, and second implementations. |
When an Interface Earns Its Place
An interface becomes justified in Meridian under two common conditions. First, a use case depends on behavior that may have more than one implementation, such as a payment-provider client or a tenant resolver with both live and test variants. Second, a service test needs a seam narrower than the concrete dependency. Both cases are consumer-driven. The service layer defines the behavior it needs, and infrastructure adapts to it; the direction does not start at the database driver or HTTP client package.
apps/billing-service/internal/application/invoices/service.gocreateResponsibility: Own the invoice use case and declare the minimum collaborator behavior it consumes.
Why now: The use case, not the storage adapter, is the place that can tell which methods are actually required.
Connects to: Sits above the future billing-service domain entities and below the HTTP transport layer.
apps/billing-service/internal/postgres/invoices_repository.gocreateResponsibility: Adapt Postgres access to the consumer-owned invoice repository contract.
Why now: Dependency inversion means storage code conforms to the application boundary, not the other way around.
Connects to: Will enforce tenant_id filters and invoice ownership rules once persistence exists.
Consumer-Owned Contracts
This lesson's version of dependency inversion is mostly about package direction. apps/*/cmd/*/main.go wires concrete values today; future application packages will describe the behaviors they consume; and outer adapters such as Postgres clients, payment-provider clients, or tenant resolvers will implement those behaviors. The interface therefore belongs closest to the use case, not inside a shared abstractions folder and not inside libs/platform, which is reserved for cross-service conventions such as config, logging, errors, response envelopes, and tenancy vocabulary.
The Decision
Meridian adopts an anti-premature-abstraction stance. Empty domain packages are intentional during scaffolding; interfaces are introduced only when a concrete use case or a test double requires them; and once they appear, the consuming application layer owns the contract. That keeps dependency inversion aligned with real tenant-scoped behavior instead of with guessed infrastructure seams.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.