Lesson 4 · Hexagonal — the shape of a v2 module · your code

Spot the leak

Part 1's payoff: a module can look perfectly hexagonal — right folders, right port interfaces — and still break the one rule that matters. Here's a real one, in conversationmgmt.

Your win: read a real constructor in your own service, spot the dependency-inversion break, and say exactly where the wiring should live instead.

The setup: it looks textbook

conversationmgmt's conversation module has the cleanest folder layout in the service: core/port/ holds every interface, infrastructure/ holds every implementation (Lesson 2). By the folder map, it's a model hexagon. Now look at the actual constructor that builds the service — read it before the explanation:

internal/conversationmgmt/modules/conversation/core/service/conversation_modifier.go
// line 3-8 — the imports
import (
    ".../conversation/core/port/repository"
    ".../conversation/core/port/service"
    ".../conversation/infrastructure/postgres"   // ← the core imports infrastructure
    ...
)

// line 31-49 — the constructor, inside package `service` (the CORE)
func NewConversationModifierService(db database.Ext, ...) service.ConversationModifierService {
    return &conversationModifierServiceImpl{
        ConversationRepo:       &postgres.ConversationRepo{},        // ← news up a concrete adapter
        ConversationMemberRepo: &postgres.ConversationMemberRepo{},  //    ...here, in the core
        ChatVendorUserRepo:     &postgres.AgoraUserRepo{},
        // ... six more concrete &postgres.XxxRepo{}
    }
}

Did you catch it?

The struct fields are typed as portsrepository.ConversationRepo (an interface). Good. But the constructor news up concrete &postgres.XxxRepo{} adapters, right here inside core/service. To do that it has to import ".../infrastructure/postgres" — line 6.

The rule it breaks The core imports infrastructure. Lesson 2's whole point was that the arrow only goes inward: adapters depend on ports, the core never depends on adapters. Here core/service depends on infrastructure/postgres — the exact reversal. The port interfaces are real, but they're not buying anything, because the core still reaches out and grabs the concrete postgres struct itself.1

Where the wiring belongs: the composition root

There should be exactly one place that knows both the port and its concrete adapter — the composition root — and it belongs at the outer edge, not in the core. Your other services get this right, in two different spots:

ServiceWhere adapters get wired to portsCore imports infra?
eureka v2cmd/server/eureka/gserver.go (manual DI at startup)No — interface files never import postgres
spikethe controller constructor NewEmailModifierServiceNo — wired at the transport edge
conversationmgmtinside core/serviceYes — the leak

In eureka, the usecase and repository interface files are pure — the concrete postgres.NewSubmissionRepo() is instantiated in gserver.go and injected inward. In spike, the concrete &repositories.EmailRepo{} is built in NewEmailModifierService at the controller edge (you'll see this exact wiring in Lesson 8). Both keep the core innocent of postgres. conversationmgmt's conversation module doesn't — the composition root leaked inward.

Why this is the Part 1 payoff Hexagonal isn't "do you have interfaces?" — conversationmgmt has plenty. It's "does the dependency actually only point inward?" The folder structure can be immaculate and the rule still broken by a single import in a constructor. Reading a hexagon critically means checking the import direction, not the folder names. That's a skill you now have — on your own code.
Nuance — this isn't the "constructor vs literal" rule You'll meet a different rule in Part 3: eureka's sqlc repos must be built with NewXxxRepo(), never a bare &Repo{}, because they hold a querier field. conversationmgmt's &postgres.ConversationRepo{} is a bare literal and that's fine — its repos are stateless struct{}. The leak here isn't the literal; it's which package is doing the newing-up. Keep the two ideas separate.
Read this next

The dependency rule

Cockburn (and Three Dots Labs) on why the arrow only points inward — the exact rule this leak breaks. Re-read with the conversationmgmt constructor in mind.

threedots.tech — Clean Architecture in Go · Cockburn — Hexagonal
→ in-repo internal/conversationmgmt/modules/conversation/core/service/conversation_modifier.go:6,31-49

Check yourself (from memory)

Q1. What exactly is the leak in conversationmgmt?

core/service imports infrastructure/postgres and constructs &postgres.XxxRepo{} itself — the dependency points outward.

Q2. Where should concrete adapters be wired to ports?

One composition root at the edge — eureka's gserver.go or spike's controller constructor — instantiates adapters and injects them inward.

Q3. The folder layout is perfectly hexagonal. Does that prove the rule holds?

Structure ≠ dependency direction. A single import in a constructor can break the rule while every folder looks textbook. Read the imports.
Recall: the conversationmgmt leak + where wiring belongs.
the break + the fix + the lesson, then reveal
The leak: conversation/core/service/conversation_modifier.go:6 imports infrastructure/postgres; :31-49 the constructor news up &postgres.ConversationRepo{} (and 7 more) inside the core. Fields are typed as ports (interfaces) — good — but the core still reaches for the concrete adapter, so the dependency points outward: core → infrastructure. The fix / where wiring belongs: one composition root at the outer edge — eureka wires in cmd/server/eureka/gserver.go; spike wires in the controller ctor NewEmailModifierService — and injects adapters inward, keeping the core innocent of postgres. The skill: hexagonal = "does the arrow only point inward?", checked by import direction, not folder names. (Separate from the Part-3 constructor-not-literal rule.)
🎯 Interview one-liner "Have you seen clean architecture done wrong?" → "Yes — a module with perfect ports and folders where the core constructor still imported the postgres package and instantiated the concrete repos itself. The interfaces were there but bought nothing, because the dependency pointed outward. The fix is a single composition root at the edge — wire the adapters in main/the transport constructor and inject them inward."
That's Part 1 — you can now read a v2 module's hexagon and judge whether it actually holds. Take the four quizzes cold, then tell me "build Part 2" for CQRS: commands, queries, and the read/write split. Ask me anything about the leak first — it's worth being certain about.

1. In-repo (verified): internal/conversationmgmt/modules/conversation/core/service/conversation_modifier.go:6 (import), :31-49 (constructor news-up). Contrast cmd/server/eureka/gserver.go (eureka wiring) and internal/spike/modules/email/controller/grpc/email_modifier_service.go:39-67 (spike wiring).