{
  "slug": "Refactoring-a-Go-Web-Service-with-Uber-FX--From-Monolith-to-Modular-Elegance-2789a9ac8ec5",
  "title": "🧠 Refactoring a Go Web Service with Uber FX: From Monolith to Modular Elegance",
  "subtitle": "This article walks through how we refactored a simple Gin-based web service into a modular, testable, and idiomatic Uber FX application.",
  "excerpt": "This article walks through how we refactored a simple Gin-based web service into a modular, testable, and idiomatic Uber FX application.",
  "date": "2025-09-26",
  "tags": [
    "Go",
    "Architecture"
  ],
  "readingTime": "5 min",
  "url": "https://medium.com/@mobinshaterian/refactoring-a-go-web-service-with-uber-fx-from-monolith-to-modular-elegance-2789a9ac8ec5",
  "hero": "https://cdn-images-1.medium.com/max/800/1*9rw_e-q29qREd5vLroqjJg.jpeg",
  "content": [
    {
      "type": "heading",
      "level": 2,
      "text": "🧠 Refactoring a Go Web Service with Uber FX: From Monolith to Modular Elegance"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Introduction"
    },
    {
      "type": "paragraph",
      "html": "In modern Go development, maintainability and lifecycle control are essential. This article walks through how we refactored a simple Gin-based web service into a modular, testable, and idiomatic Uber FX application. We’ll cover lifecycle hooks, dependency injection, behavioral testing, and configuration-driven design."
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*9rw_e-q29qREd5vLroqjJg.jpeg",
      "alt": "🧠 Refactoring a Go Web Service with Uber FX: From Monolith to Modular Elegance",
      "caption": "",
      "width": 1024,
      "height": 1024
    },
    {
      "type": "paragraph",
      "html": "In our previous article, we developed a simple RESTful API using Golang. In this article, we aim to extend that work by integrating Uber FX to facilitate dependency injection more effectively."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "🧱 Original Structure"
    },
    {
      "type": "paragraph",
      "html": "The original project used a traditional <code>cmd/server/main.go</code> entry point with hardcoded route registration and server startup logic. While functional, it lacked modularity, lifecycle control, and testability."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "🔄 Refactoring with Uber FX"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "1. Modular main.go with FX Composition"
    },
    {
      "type": "paragraph",
      "html": "We replaced the monolithic startup with a clean FX composition:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func main() {\n    fx.New(fx.Provide(config.NewConfig, health.New, server.NewGinEngine, server.CreateHTTPServer,),\n    fx.Invoke(server.RegisterRoutes, server.StartHTTPServer,),\n    ).Run()\n}"
    },
    {
      "type": "paragraph",
      "html": "This separates concerns and lets FX manage lifecycle and dependency injection."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "🧩 fx.Provide: Declaring Dependencies"
    },
    {
      "type": "paragraph",
      "html": "<code>fx.Provide(...)</code> tells FX <strong>how to construct the values</strong> your app needs. You pass it constructor functions, and FX will automatically call them in the right order, resolving dependencies."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Example:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "fx.Provide(    config.NewConfig,       // returns *Config    server.NewGinEngine,    // returns\n*gin.Engine)"
    },
    {
      "type": "paragraph",
      "html": "This means:"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "FX will call <code>NewConfig()</code> and store the result.",
        "Then it will call <code>NewGinEngine()</code> and inject any dependencies it needs (like <code>*Config</code> if required)."
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Think of it as:"
    },
    {
      "type": "quote",
      "html": "<em>“Here’s how to build the parts of my app.”</em>"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "🔧 fx.Invoke: Wiring Behavior"
    },
    {
      "type": "paragraph",
      "html": "<code>fx.Invoke(...)</code> tells FX <strong>what to do with the values it built</strong>. You pass it functions that receive dependencies and perform actions—like registering routes or starting a server."
    },
    {
      "type": "code",
      "lang": "go",
      "code": "fx.Invoke(    server.RegisterRoutes,    server.StartHTTPServer,)"
    },
    {
      "type": "paragraph",
      "html": "This means:"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "FX will call <code>RegisterRoutes(engine, health, config)</code> using the values it built.",
        "Then it will call <code>StartHTTPServer(lifecycle, server)</code> to hook into startup/shutdown."
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Think of it as:"
    },
    {
      "type": "quote",
      "html": "<em>“Now that you’ve built everything, here’s what to do with it.”</em>"
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*It5uP4S4b3qY4ONn7VAH4A.png",
      "alt": "🧠 Refactoring a Go Web Service with Uber FX: From Monolith to Modular Elegance",
      "caption": "",
      "width": 733,
      "height": 203
    },
    {
      "type": "heading",
      "level": 2,
      "text": "2. Lifecycle-Aware Server Startup"
    },
    {
      "type": "paragraph",
      "html": "We split server creation and startup into two functions:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func CreateHTTPServer(engine *gin.Engine, cfg *config.Config) *http.Server {\n    return &http.Server{\n        Addr:\n        fmt.Sprintf(\"%s:%d\", cfg.HTTPAddress, cfg.HTTPPort), Handler: engine,\n    }\n}\nfunc StartHTTPServer(lc fx.Lifecycle, srv *http.Server) {\n    lc.Append(fx.Hook{\n        OnStart: func(ctx context.Context) error {\n            log.Printf(\"🚀 HTTP server starting on %s\", srv.Addr) go func() {\n                if err := srv.ListenAndServe();\n                err != nil && err != http.ErrServerClosed {\n                    log.Fatalf(\"server error: %v\", err)\n                }\n            }\n            () return nil\n        }, OnStop: func(ctx context.Context) error {\n            log.Println(\"🛑 Shutting down HTTP server...\") shutdownCtx, cancel :=\n            context.WithTimeout(ctx, 5*time.Second) defer cancel() return srv.Shutdown(shutdownCtx)\n        },\n    })\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "3. Dynamic Route Registration"
    },
    {
      "type": "paragraph",
      "html": "Routes are registered via an FX <code>Invoke</code>:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func RegisterRoutes(engine *gin.Engine, health *health.Health, cfg *config.Config) {\n    log.Println(\"🚀 Registering routes...\") engine.GET(\"/health\", health.Handle) // Set Swagger\n    metadata dynamically docs.SwaggerInfo.Title = \"My API\" docs.SwaggerInfo.Version = \"1.0\"\n    docs.SwaggerInfo.Description = \"This is a sample API with Gin and Swagger.\"\n    docs.SwaggerInfo.Host = fmt.Sprintf(\"%s:%d\", cfg.HTTPAddress, cfg.HTTPPort)\n    docs.SwaggerInfo.BasePath = \"/\" docs.SwaggerInfo.Schemes = []string{\n        \"http\"\n    }\n    // or {\n        \"https\"\n    }\n    in production engine.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "4. Config-Driven Port and Address"
    },
    {
      "type": "paragraph",
      "html": "We introduced a <code>Config</code> struct:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "type Config struct {\n    HTTPAddress string\n    HTTPPort int\n}\nfunc NewConfig() *Config {\n    return &Config{\n        HTTPAddress: \"127.0.0.1\", HTTPPort:\n        4000,\n    }\n}"
    },
    {
      "type": "paragraph",
      "html": "This allows dynamic binding and future support for env-based configuration."
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*CfiCpm5XnoR0mMkCj83IBw.png",
      "alt": "🧠 Refactoring a Go Web Service with Uber FX: From Monolith to Modular Elegance",
      "caption": "",
      "width": 1536,
      "height": 1024
    },
    {
      "type": "heading",
      "level": 2,
      "text": "🧪 Behavioral Testing with FX"
    },
    {
      "type": "paragraph",
      "html": "We created a test harness that spins up the FX app and hits real endpoints:"
    },
    {
      "type": "code",
      "lang": "go",
      "code": "func StartHTTPServer() *fx.App {\n    a := app.NewApp() go a.Run() time.Sleep(300 * time.Millisecond) // give server time to start\n    return a\n}\nfunc WithTestServer(t *testing.T, testFunc func()) {\n    a := StartHTTPServer() defer a.Stop(context.Background()) testFunc()\n}\nfunc TestHealthEndpoint(t *testing.T) {\n    t.Parallel() WithTestServer(t, func() {\n        cfg, err := config.NewConfig() if err != nil {\n            t.Fatalf(\"Failed to load config: %v\", err)\n        }\n        addr := fmt.Sprintf(\"http://%s:%d\", cfg.HTTPAddress, cfg.HTTPPort) resp, err :=\n        http.Get(addr + \"/health\") if err != nil {\n            t.Fatalf(\"Failed to send GET request: %v\", err)\n        }\n        defer resp.Body.Close() if resp.StatusCode != http.StatusOK {\n            t.Errorf(\"Expected status 200 OK, got %d\", resp.StatusCode)\n        }\n        body, err := io.ReadAll(resp.Body) if err != nil {\n            t.Fatalf(\"Failed to read response body: %v\", err)\n        }\n        // Parse JSON\n        var data map[string]string err = json.Unmarshal(body, &data) if err != nil {\n            t.Fatalf(\"Expected JSON response, got error: %v\", err)\n        }\n        // Validate the message message, ok := data[\"message\"] if !ok {\n            t.Fatalf(\"Missing 'message' field in response: %v\", data)\n        }\n        if message != \"OK\" {\n            t.Errorf(\"Expected message 'hello world', got '%s'\", message)\n        }\n    })\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "🧠 Lessons Learned"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "<strong>Uber FX</strong> simplifies lifecycle management and dependency injection.",
        "<strong>Modular design</strong> improves testability and clarity.",
        "<strong>Behavioral tests</strong> validate real-world behavior, not just isolated logic.",
        "<strong>Configuration</strong> makes your app flexible and production-ready."
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Github commit"
    },
    {
      "type": "embed",
      "provider": "youtube",
      "url": "https://www.youtube.com/embed/vjf0GuUDGRU?feature=oembed"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Conclusion"
    },
    {
      "type": "paragraph",
      "html": "Refactoring a Gin-based Go web service with Uber FX transforms it from a monolithic structure into a modular, maintainable, and production-ready application. By leveraging FX’s dependency injection and lifecycle management capabilities, we addressed key limitations of the original setup — hardcoded logic, poor testability, and lack of clear separation of concerns — while enhancing clarity and scalability."
    },
    {
      "type": "paragraph",
      "html": "The journey began with replacing a monolithic <code>main.go</code> with FX composition, where <code>fx.Provide</code> and <code>fx.Invoke</code> systematically declare dependencies and wire application behavior. This modular approach ensures FX resolves dependencies automatically, eliminating manual wiring and reducing errors. Lifecycle hooks, implemented through <code>fx.Lifecycle</code>, further improved robustness by enabling graceful server startup and shutdown, critical for reliability in production environments."
    },
    {
      "type": "paragraph",
      "html": "Dynamic route registration and config-driven port/address settings underscored FX’s flexibility: routes are now cleanly separated from server logic, and configurations (e.g., HTTP address/port) are centralized, simplifying future adjustments like environment-specific tuning. Equally impactful was the shift to behavioral testing, where the FX app itself is spun up to validate real endpoint interactions, ensuring that the system behaves as expected in practice — not just in isolated unit tests."
    },
    {
      "type": "paragraph",
      "html": "The lessons learned reinforce Uber FX as a powerful tool for modern Go development. It streamlines lifecycle control, makes dependency management explicit, and fosters modular design — all of which are foundational for building maintainable, testable, and idiomatic applications. By adopting FX, the project gains a robust architecture that supports growth, whether adding new services, integrating with external tools (like SonarQube), or adapting to evolving requirements."
    },
    {
      "type": "paragraph",
      "html": "This refactoring marks a significant step toward aligning the service with best practices in Go and AI-driven innovation, ensuring it remains adaptable and scalable as it evolves to meet real-world societal needs. For further enhancements, the next steps could include integrating advanced configuration sources (e.g., environment variables, external config files) or expanding lifecycle hooks to manage additional services, solidifying the application’s foundation for future success."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "A message from our Founder"
    },
    {
      "type": "paragraph",
      "html": "<strong>Hey, </strong><a href=\"https://linkedin.com/in/sunilsandhu\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Sunil</strong></a><strong> here.</strong> I wanted to take a moment to thank you for reading until the end and for being a part of this community."
    },
    {
      "type": "paragraph",
      "html": "Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? <strong>We don’t receive any funding, we do this to support the community. ❤️</strong>"
    },
    {
      "type": "paragraph",
      "html": "If you want to show some love, please take a moment to <strong>follow me on </strong><a href=\"https://linkedin.com/in/sunilsandhu\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>LinkedIn</strong></a><strong>, </strong><a href=\"https://tiktok.com/@messyfounder\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>TikTok</strong></a>, <a href=\"https://instagram.com/sunilsandhu\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Instagram</strong></a>. You can also subscribe to our <a href=\"https://newsletter.plainenglish.io/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>weekly newsletter</strong></a>."
    },
    {
      "type": "paragraph",
      "html": "And before you go, don’t forget to <strong>clap</strong> and <strong>follow</strong> the writer️!"
    }
  ]
}
