Dependency injection in Go with Uber-go/fx
41–50 of 96 posts
Re: Dependency injection in Go with Uber-go/fx
#42At the time we wrote Fx, Uber had ~1500 engineers writing Go. The company had 25 million lines of Go, spread across more than a thousand microservices and an unknown number of shared libraries (likely hundreds, perhaps as many as a thousand). Nearly every project was in a separate git repository, with effectively no tools to make large cross-repository refactorings. As an engineering organization, we struggled to make relatively simple cross-cutting changes.
For example, we spent years rolling out distributed tracing. The actual change required was simple: upgrade all your dependencies to something recent-ish, add the tracing library, construct a tracer in main (or something main-adjacent), use the tracer to construct an RPC interceptor, and add the interceptor to your API server. All in, we're talking about ~20 lines of code and a dependency upgrade. It took multiple TPMs, spreadsheets, quarterly planning, and several high-level edicts to get this mostly done.
Why were changes so painful? At root, because nobody cared much about most of the Go repositories. From the perspective of the teams who nominally owned the code (often after several reorganizations over the years), the code worked fine and solved the business purpose - why invest time in changing anything? On the ground, changes were painful. Go's simplicity makes semantic versioning _very_ restrictive, so trying to pull in a year's worth of dependency updates often produced a variety of breakages. (Keep in mind that many of these libraries were used only in a handful of projects and weren't particularly carefully designed or maintained.) Taking on all this pain to change 20 lines of code in main was a difficult sell.
Fx codified some basic back-compat best practices (if you're paranoid) - mostly param and result structs, so constructors have more flexibility to add inputs and outputs. Fx also made most of these problems a negotiation directly between library authors, leaving the microservice team out of the picture: the "standard Uber stuff" package provides a distributed tracer, and the "RPC stuff" package takes an _optional_ tracer and installs the appropriate interceptor. No changes to main or application logic necessary, just a dependency update (which is hopefully safer, since more libraries are forced to follow better semver practices). The reflection-based wiring came with lots of magic and downsides, but the tradeoff was worth it across the engineering organization - it made us _overall_ more able to change our own systems.
Bluntly, IMO Fx made individual codebases less understandable (especially codebases carefully maintained by engineers who like Go). It made the whole company's code more maintainable. The bulk of the Go engineers at the company agreed (the developer experience org tracked NPS, which went from double-digit negative to +40ish).
In the years since Fx, I left Uber and the company has moved most of their Go to a monorepo. I'm not sure what the current cost/benefit tradeoff of this approach is.
Re: Dependency injection in Go with Uber-go/fx
#43"Dependency Injection" should be just shorthand jargon for "pass a pointer/reference to the dependency into the constructor". Unfortunately, thanks to enterprisey-OO zealotry, it's become a terrible monstrosity of frameworks, obfuscation-by-configurable-injection, and other terrible practices. Every single application I've worked on where DI (in practice, not theory) is in use has been fragile, hard to debug, hard to…
Topological sort and run. That’s all there is to dependency injection and I cannot imagine a graph so large that I would care for toposort
Re: Dependency injection in Go with Uber-go/fx
#44I've never been convinced by DI frameworks. I've always enjoyed the fact that the Go ecosystem leans away from them and I would hope things stay that way. In my experience they just obfuscate what should be a straightforward, explicit process of setting up your app in main. The less magic happening there the better.
Re: Dependency injection in Go with Uber-go/fx
#45Earlier quoted context omitted.
> What does dependency injection give you that a simple combination of Singletons, Constructors, and Factories doesn't? Easy refactoring of an interface/constructor? I've been using go for a while, and I decided to follow the advise of not using a DI for my project. Every time I refactor a constructor, it is a fun hunt to make the same changes everywhere I'm using that constructor.
Doesn't the compiler tell you exactly what needs to change?
Re: Dependency injection in Go with Uber-go/fx
#46Go doesn’t really have this problem so I’m not convinced it needs DI at all.
C++ doesn’t really have much DI mindshare because it has templates (and macros).
Goice (Guice for Go) anyone?
Re: Dependency injection in Go with Uber-go/fx
#47I've used dependency injection heavily in Java in previous jobs, and, later, spent a few years doing Golang at another job. I never missed it. What does dependency injection give you that a simple combination of Singletons, Constructors, and Factories doesn't? I feel like the only thing you get is the ability to combine multiple independent dependency trees without having to make a sane structure. Kind of like, what…
Re: Dependency injection in Go with Uber-go/fx
#48Re: Dependency injection in Go with Uber-go/fx
#49I've used dependency injection heavily in Java in previous jobs, and, later, spent a few years doing Golang at another job. I never missed it. What does dependency injection give you that a simple combination of Singletons, Constructors, and Factories doesn't? I feel like the only thing you get is the ability to combine multiple independent dependency trees without having to make a sane structure. Kind of like, what…
Fx author (in part) here - replied in a separate comment. I mostly agree with you, but DI offers some ecosystem-wide benefits that may be useful in some environments.
Re: Dependency injection in Go with Uber-go/fx
#50Given that Go modules can depend on private interfaces that specify only the methods they need, dependency injection of the pass-dependencies-to-the-constructor style seems almost built in. Not automated as such, but easy.