Skip to content

Proposal: Migrate to uber-go/fx for Dependency Injection #253

Open
@diyor28

Description

@diyor28

Refactoring IOTA SDK to Uber‑FX

1. Background

Today the IOTA SDK hand‑wires constructors, context providers, and HTTP handlers via reflection (pkg/di, H/Invoke, composables). As modules grow (core, CRM, finance, warehouse, …) this “glue” becomes unwieldy:

  • Every binary (server, migrate, seed, etc.) repeats plumbing.
  • Dependencies aren’t explicit in function signatures.
  • Lifecycle (start/stop hooks) is custom and ad hoc.

2. Why Uber‑FX?

fx (built on dig) offers:

  • Automatic wiring of constructors (fx.Provide)
  • Module grouping (fx.Options) per bounded context
  • Lifecycle hooks (fx.Lifecycle for OnStart/OnStop)
  • Explicit dependencies in each constructor’s signature
  • Built‑in introspection of your dependency graph
  • Strong type‑safety and better testability

3. Key Benefits for IOTA SDK

  1. Modular initialization
    • Each domain (core, CRM, finance, warehouse, website, …) becomes an fx.Option you can register independently.
  2. Minimal boilerplate
    • One fx.New(...) per binary—no more manual builder code.
  3. Clear lifecycles
    • Attach Postgres pool shutdown, WebSocket broker stop, HTTP server start/stop via fx.Lifecycle.
  4. Easier testing
    • Swap out individual constructors (fx.Testing) or provide mocks via fx.Provide.
  5. DDD‑friendly
    • Your domain constructors remain pure (func NewOrderService(repo OrderRepo) *OrderService), but now wired automatically.

4. Proposed Architecture

4.1. Core Binary (cmd/server/main.go)

package main

import (
  "context"
  "log"

  "go.uber.org/fx"

  "github.com/iota-uz/iota-sdk/internal/assets"
  "github.com/iota-uz/iota-sdk/internal/server"
  "github.com/iota-uz/iota-sdk/modules"
  "github.com/iota-uz/iota-sdk/pkg/application"
  "github.com/iota-uz/iota-sdk/pkg/configuration"
  "github.com/iota-uz/iota-sdk/pkg/eventbus"
  "github.com/iota-uz/iota-sdk/pkg/logging"

  corefx "github.com/iota-uz/iota-sdk/fxmodules/core"
  crmfx "github.com/iota-uz/iota-sdk/fxmodules/crm"
  financefx "github.com/iota-uz/iota-sdk/fxmodules/finance"
  warehousefx "github.com/iota-uz/iota-sdk/fxmodules/warehouse"
  serverfx "github.com/iota-uz/iota-sdk/fxmodules/server"
)

func main() {
  app := fx.New(
    // —— Global Providers ——
    fx.Provide(
      configuration.New,            // *Configuration, error
      logging.NewLogger,            // *logrus.Entry
      func(cfg *configuration.Configuration) (*pgxpool.Pool, error) {
        return pgxpool.New(context.Background(), cfg.Database.Opts)
      },
      eventbus.NewPublisher,        // *eventbus.Publisher
      application.New,              // *application.Application
    ),

    // —— Domain Modules ——
    corefx.Module(),
    crmfx.Module(),
    financefx.Module(),
    warehousefx.Module(),

    // —— HTTP Server & Static Assets ——
    serverfx.Module(),

    // —— Application Initialization ——
    fx.Invoke(func(app *application.Application) error {
      // Load/Deregister built-in modules, register nav links & HashFS assets
      if err := modules.Load(app, modules.BuiltInModules...); err != nil {
        return err
      }
      app.RegisterNavItems(modules.NavLinks...)
      app.RegisterHashFsAssets(assets.HashFS)
      return nil
    }),
  )

  app.Run() // Blocks until shutdown
}

4.2. Example fx.Module (e.g. CRM)

// fxmodules/crm/module.go
package crmfx

import "go.uber.org/fx"

func Module() fx.Option {
  return fx.Options(
    // Service + repo constructors
    fx.Provide(
      crmPersistence.NewClientRepository,   // func NewClientRepository(...) ClientRepo
      crmService.NewClientService,          // func NewClientService(repo ClientRepo) *ClientService
      presentation.NewClientController,     // func NewClientController(svc *ClientService) *ClientController
    ),

    // Register HTTP routes / GraphQL
    fx.Invoke(presentation.RegisterClientRoutes),
  )
}

4.3. HTTP Server Module

// fxmodules/server/module.go
package serverfx

import (
  "context"
  "log"

  "go.uber.org/fx"
  "github.com/iota-uz/iota-sdk/internal/server"
  "github.com/iota-uz/iota-sdk/pkg/configuration"
)

func Module() fx.Option {
  return fx.Options(
    fx.Provide(
      server.NewDefault, // func NewDefault(opts *server.Options) (*server.Server, error)
    ),
    fx.Invoke(func(lc fx.Lifecycle, srv *server.Server, cfg *configuration.Configuration) {
      lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
          log.Printf(">> Listening on %s", cfg.Address())
          return srv.Start(cfg.SocketAddress)
        },
        OnStop: srv.Stop,
      })
    }),
  )
}

5. Migration Roadmap

  1. Extract Core Constructors
    • Move NewConfig, NewLogger, NewPostgresDB, NewEventPublisher, application.New into fx.Provide‑compatible functions.
  2. Bootstrap cmd/server
    • Replace hand‑wired main.go with the fx-based pattern above.
  3. Gradual Module Migration
    • For each domain (core, CRM, finance, warehouse, website…):
      • Create fxmodules/<domain>/Module()
      • Move all NewXxx constructors and RegisterRoutes into it.
  4. Additional Binaries
    • Apply same pattern to cmd/migrate, cmd/seed, cmd/document, cmd/collect‑logs, wiring only the providers they need.
  5. Sunset pkg/di
    • Once all constructors live under fx, remove pkg/di and H/Invoke.
  6. CI & Testing
    • Leverage fx.Testing to inject mocks.
    • Retain existing NewXxx() constructors for backward compatibility in unit tests.

6. Risks & Mitigations

  • Startup‑time errors: missing providers surface at runtime.
    Mitigation: enable fx’s graph printing (fx.New(fx.PrintDeps())) in dev.
  • Learning curve for contributors.
    Mitigation: provide a “Getting Started” doc + examples.
  • Longer build times when adding many constructors.
    Mitigation: group into coarse‑grained modules; merge related constructors.

7. Next Steps

  • Prototype cmd/server migration in a feature branch.
  • Update documentation and developer onboarding.
  • Review and merge core infra changes.
  • Roll out domain modules incrementally, validating with smoke tests.

By following this plan, IOTA SDK will gain a scalable, maintainable bootstrap system, clearer lifecycles, and faster developer onboarding—while preserving our DDD boundaries and testability.

Metadata

Metadata

Assignees

No one assigned

    Labels

    draftIssue not complete & is being worked onenhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions