-
Notifications
You must be signed in to change notification settings - Fork 232
Open
Labels
Description
Current Architecture
Structure
internal/
application/
store.go
service.go
handler.go
user/
store.go
service.go
handler.go
groups/
store.go
service.go
handler.go
system/
services/
applicationservice.go # Initialization for application module
userservice.go # Initialization for user module
groupservice.go # Initialization for groups module
idpservice.go # Initialization for IDP module
...
Problems with Current Approach
1. Code Scattering - Violation of Module Cohesion
Related code for a single module is split across multiple locations:
internal/
application/ # Business logic (store, service, handler)
store.go
service.go
handler.go
system/
services/
applicationservice.go # Initialization logic (far from module)
Impact:
- ❌ Developers must navigate between distant directories to understand a module's complete lifecycle
- ❌ Changes to a module's initialization require editing files outside the module's directory
- ❌ Violates the principle of high cohesion - related code should be together
- ❌ Makes it harder to understand module boundaries and ownership
- ❌ Reduces code discoverability - "Where is the application module initialized?"
Example:
To understand the application module, a developer must look at:
1. /internal/application/store.go (data layer)
2. /internal/application/service.go (business logic)
3. /internal/application/handler.go (HTTP layer)
4. /internal/system/services/applicationservice.go (initialization) ← Far away!
2. Dependency Injection Issues
The centralized service initialization pattern makes dependency management problematic.
Problem A: Hidden Dependencies
// internal/system/services/userservice.go
func InitializeUserService(mux *http.ServeMux, db *sql.DB) *user.Service {
// Where does this come from? Not clear from the signature
appSvc := GetApplicationService() // Global state? Package variable?
store := user.NewStore(db)
service := user.NewService(store, appSvc)
handler := user.NewHandler(service)
handler.RegisterRoutes(mux)
return service
}Issues:
- Dependencies are not explicit in function signatures
- Unclear where
appSvccomes from - Potential for global state and coupling
- Hard to trace dependency flow
Problem B: Testing Complexity
// To test user service initialization:
func TestInitializeUserService(t *testing.T) {
// Must set up the entire system/services infrastructure
// Cannot easily mock just the application service dependency
// Hard to isolate the user module for testing
}Issues:
- ❌ Hard to test individual module initialization in isolation
- ❌ Must mock/stub the entire service initialization system
- ❌ Cannot easily inject test dependencies for a single module
- ❌ Integration tests become the only viable option
3. Scalability Concerns
As the IAM platform grows, this pattern becomes increasingly problematic:
internal/
system/
services/
applicationservice.go
userservice.go
groupservice.go
idpservice.go
roleservice.go # New module
permissionservice.go # New module
auditservice.go # New module
sessionservice.go # New module
... # Directory keeps growing
Issues:
- ❌ The
/internal/system/services/directory becomes a dumping ground - ❌ No clear ownership - who maintains
system/services? - ❌ Adding a new module requires changes in multiple places:
- Create module files in
/internal/{module}/ - Create initialization file in
/internal/system/services/ - Update service orchestration logic
- Update dependency wiring
- Create module files in
- ❌ Merge conflicts increase as team size grows
- ❌ Difficult to assign module ownership to specific teams
4. Encapsulation Violation
Modules cannot fully control their own lifecycle:
// internal/system/services/applicationservice.go
func InitializeApplicationService(mux *http.ServeMux, db *sql.DB) *application.Service {
store := application.NewStore(db)
service := application.NewService(store)
// Module-specific setup must happen here, outside the module
// What if application module needs special initialization?
// What if it needs to run migrations?
// What if it needs to warm up caches?
handler := application.NewHandler(service)
handler.RegisterRoutes(mux)
return service
}Issues:
- ❌ Initialization details leak into the central services directory
- ❌ Module-specific setup (DB migrations, cache warming, feature flags) has no natural home
- ❌ Forces all modules to follow the same initialization pattern, even if inappropriate
- ❌ Breaks the "package owns its own concerns" principle
- ❌ Module authors cannot fully encapsulate their module's behavior
5. Code Organization Anti-patterns
internal/
system/
services/
applicationservice.go # Knows about application internals
userservice.go # Knows about user internals
groupservice.go # Knows about groups internals
This creates a "God package" that:
- ❌ Knows about the internals of all modules
- ❌ Has dependencies on every module in the system
- ❌ Becomes a bottleneck for changes
- ❌ Violates the Single Responsibility Principle
- ❌ Creates tight coupling across the entire codebase
Suggested Improvement
Proposed model
cmd/
server/
main.go
servicemanager.go # orchestration logic
internal/
application/
store.go
service.go
handler.go
init.go # ← Module initialization logic
user/
store.go
service.go
handler.go
init.go # ← Module initialization logic
groups/
store.go
service.go
handler.go
init.go # ← Module initialization logic
idp/
store.go
service.go
handler.go
init.go. # ← Module initialization logic
Implementation
Module Level: init.go in each package
// internal/application/init.go
package application
import (
"net/http"
)
// Initialize sets up the application module and registers its routes
func Initialize(mux *http.ServeMux) *Service {
service := NewService()
handler := NewHandler(service)
// Any other Initialisation.
handler.RegisterRoutes(mux)
return service
}Orchestration
// cmd/server/servicemanager.go
package main
import (
"net/http"
"yourmodule/internal/application"
"yourmodule/internal/user"
"yourmodule/internal/groups"
"yourmodule/internal/idp"
)
// Container holds all initialized services available for cross-module access if needed
type Services struct {
Application *application.Service
User *user.Service
Groups *groups.Service
IDP *idp.Service
....
}
// initializes all modules in the correct dependency order
func InitializeServices(mux *http.ServeMux) {
s := &Services{}
// Initialize modules in dependency order
// Each module handles its own wiring and route registration
s.Application = application.Initialize(mux)
s.User = user.Initialize(mux, s.Application)
s.Groups = groups.Initialize(mux, s.User)
s.IDP = idp.Initialize(mux, s.User, s.Groups)
}// cmd/api/main.go
package main
import (
"log"
"net/http"
"os"
_ "github.com/lib/pq"
)
func main() {
// Setup infrastructure
mux := http.NewServeMux()
// Initialize all modules through container
InitializeServices(mux)
// Start server
}Benefits of Proposed Approach
1. High Cohesion ✅
- All code related to a module lives in the module's directory
- Easy to find everything related to a specific module
- Clear module boundaries
2. Explicit Dependencies ✅
// Crystal clear what user module needs
func Initialize(mux *http.ServeMux, appSvc *application.Service) *Service- Dependencies are visible in function signatures
- Type system enforces correct dependency injection
- Easy to trace dependency flow
3. Clear Dependency Order ✅
s.Application = application.Initialize(mux)
s.User = user.Initialize(mux, s.Application) // Must come after application
s.Groups = groups.Initialize(mux, s.User) // Must come after user- Dependency order is explicit in orchestration code
- Compiler helps prevent circular dependencies
- Easy to understand initialization flow
4. Better Testability ✅
// Easy to test user module in isolation
func TestUserInitialize(t *testing.T) {
mux := http.NewServeMux()
mockAppSvc := &MockApplicationService{}
userSvc := user.Initialize(mux, mockAppSvc)
// Test assertions
}- Modules can be tested in isolation
- Easy to inject mock dependencies
- Unit tests become straightforward
5. Module Encapsulation ✅
- Modules fully control their own lifecycle
- Module-specific setup stays in the module
- Can vary initialization patterns per module if needed
6. Scalability ✅
- Adding a new module requires:
- Create module directory with
init.go - Add one line to orchestration
- Create module directory with
- No "God package" that knows about everything
- Clear ownership per module
7. Better Code Organization ✅
- Follows Go best practices (package-level initialization)
- Aligns with idiomatic Go patterns
- Easier onboarding for new developers
Version
No response