Skip to main content

Ports & Adapters — Interface Your App, Adapt the World

Port = interface your application defines. Adapter = implementation connecting to the outside world. Swap adapters without touching the app.

When to use

  • When you need to swap external systems (DB, email, messaging) without modifying business logic
  • When testing requires in-memory replacements for external dependencies

Tradeoffs

  • One interface per external boundary (more files, more wiring)
  • DI container complexity grows with number of adapters
// Port — defined in domain
type NotificationPort interface{ Notify(userID, msg string) error }

// Adapter 1: in-memory (test)
type InMemoryNotifier struct{ Sent []string }
func (n *InMemoryNotifier) Notify(uid, msg string) error {
n.Sent = append(n.Sent, msg); return nil
}

// Adapter 2: real (infrastructure)
type EmailNotifier struct{ client *smtp.Client }
func (e *EmailNotifier) Notify(uid, msg string) error {
return sendEmail(e.client, uid, msg)
}

// Wiring at startup (main)
func main() { app := NewApp(NewEmailNotifier(smtpClient)) }

Gotcha: Primary adapters DRIVE the app (HTTP handler calls your app). Secondary adapters are DRIVEN by the app (your app calls the DB). The direction matters for testing.