Microservice-Ready Architecture: Decoupling User Lookup
The Problem You Identified ❌
Current Implementation (Tight Coupling):- ❌ Every new module needs user module code
- ❌ Can’t deploy modules independently
- ❌ Circular dependency risk
- ❌ Not true microservice architecture
- ❌ Violates Dependency Inversion Principle
The Solution: Shared Kernel + Anti-Corruption Layer ✅
Implementation Steps
Step 1: Create Shared Kernel Interface
File:internal/shared/application/port/user_lookup.go
- ✅ Lives in
internal/shared/application/port(shared kernel) - ✅ No dependencies on any module
- ✅ Minimal interface (only what’s needed)
- ✅ Can be implemented by ANY service (user, LDAP, external API, etc.)
Step 2: Create Anti-Corruption Layer in User Module
File:internal/modules/user/adapter/outbound/external/user_lookup_adapter.go
- ✅ Lives in user module (owns the adaptation)
- ✅ Implements shared kernel interface
- ✅ Maps between user module’s domain and shared kernel
- ✅ Other modules don’t know about this adapter
Step 3: Update Container
File:internal/app/container.go
- ✅ Only depends on shared kernel
- ✅ No dependency on user module
- ✅ Can be implemented by any service
Step 4: Update Middleware
File:internal/api/http/middleware/audit_context.go
- ✅ Only depends on shared kernel
- ✅ No dependency on user module
- ✅ Works with ANY implementation of UserLookupService
Step 5: Wire It Up in Main
File:cmd/server/main.go
- ✅ Only
main.goknows about the adapter - ✅ Modules are completely decoupled
- ✅ Can swap implementations easily
Benefits of This Approach
1. True Loose Coupling ✅
2. Microservice Ready ✅
Each module can be deployed independently:3. Easy to Test ✅
4. Swappable Implementations ✅
Dependency Graph
Before (Tight Coupling):
After (Loose Coupling):
Migration Path
Current State:
Target State:
Migration Steps:
- ✅ Create shared kernel interface (
internal/shared/application/port/user_lookup.go) - ✅ Create adapter in user module (
internal/modules/user/adapter/outbound/external/user_lookup_adapter.go) - ✅ Update container to use shared interface
- ✅ Update middleware to use shared interface
- ✅ Update main.go to wire adapter
- ✅ Update leave module to use container.UserLookup
Answer to Your Question
“Does this mean every new microservice module we build in the future needs to include this user service in user module? Is this still loose coupled design?”Answer:
With Current Implementation (Before Refactor): NO ❌
- Every module would depend on user module
- NOT loose coupling
- NOT microservice-ready
With Shared Kernel Approach (After Refactor): YES ✅
- Modules depend on shared kernel interface (just a contract)
- User module provides ONE implementation
- Other implementations possible (gRPC, LDAP, etc.)
- TRUE loose coupling
- Microservice-ready
http.Handler interface in Go - everyone uses it, but no one depends on a specific implementation.
Real-World Example: How Google Does It
Google’s internal services use a similar pattern:Summary
- ✅ Shared Kernel - Common interfaces, no implementation
- ✅ Anti-Corruption Layer - Adapts module to shared kernel
- ✅ Dependency Inversion - Depend on abstractions, not concretions
- ✅ Microservice Ready - Can swap implementations (local, gRPC, HTTP)
- ✅ Truly Decoupled - Modules don’t know about each other