Skip to content

Service Initialization Refactoring #456

@senthalan

Description

@senthalan

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 appSvc comes 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:
    1. Create module files in /internal/{module}/
    2. Create initialization file in /internal/system/services/
    3. Update service orchestration logic
    4. Update dependency wiring
  • ❌ 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:
    1. Create module directory with init.go
    2. Add one line to orchestration
  • 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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions