Google Guice is a dependency injection library for Java and I frequently used it on a number of Java services. Compared to Spring, I liked how simple and narrow focused on just dependency injection it was. However, I often times saw developers using it in incorrect or non-ideal patterns that increased boilerplate or were just wrong.
These are all recommendations that I’ve accumulated over several years at working at Amazon watching engineers and sometimes myself improperly leverage Google Guice.
Recommendations
When to reuse modules
If you’ve got multiple different components (e.g. a service API, workers, or other processes) then your Guice modules can be reused and shared to reduce boilerplate code.
Break down your bindings into modules with categories:
- Application-specific - These are bindings are for a specific application (e.g. FooServiceModule, FooWorkerModule) and are the top level module that imports everything else
- Environment-specific - Contains any bindings that make I/O calls that are shared by may differ between apps, for example AWS clients, secret providers, etc.
- Feature-specific - Everything else falls in here. Organize them based on related bindings. Like AuthenticationModule, AuditLogModule, FooFeatureModule.
Prefer Interfaces over concrete classes
The purpose of dependency injection is to minimize coupling between different components. If your class binds to concrete classes, then you’re preventing others from swapping out the implementations based on the environment.
Considerations
If it’s impractical to create separate implementations for a given class, such as pure functional class with no I/O or other external dependencies such as a helper class, then it’s okay to inject the concrete class.
If you need to have multiple copies of the same effective class, then use a @Named instead.
Example
|
|
In the above example, the Guice binding returns a concrete class type instead of an interface. This forces consuming classes to refer to the concrete class instead of the generic interface. When writing unit tests, now you’re forced to construct the real class instead of swapping out a mocked up implementation.
Avoid @Named to signal implementation when irrelevant
@Named bindings can suffer from the same problem as using concrete classes vs interfaces. Don’t use @Named as a way to signal what specific implementation a class is, instead only add @Named when you need to have two copies. For example, use it to delineate two different AWSCredentialProviders to two different AWS accounts.
Example
I found one service that using @Named to signal the effective type of the class. There was a service client that was exposed by the Guice config that was different depending on the process it was running in. A long-running service got a client that had a time limited cached, whereas a short-term scheduled job process got a client that cached for the entire lifetime of the job.
|
|
In the above example, there was only ever one type of FooClient that should be available in each process, but depending on the process a different FooClient should be loaded. By including the type in the @Named qualification, it caused the rest of the Guice graph to become more complicated because they all had to be aware of the type instead of being agnostic.
In the below alternative, the Guice binding becomes generic to only expose an abstract FooClient and it internalizes the logic to decide the caching strategy. The rest of the code becomes simpler.
Better:
|
|
Or create two separate Guice modules for each environment that return the correct type and avoid any runtime configuration:
|
|
Avoid bind(Foo.class).toInstance(new Foo(…))
In the following example, I’m binding the class type Foo.class to an instance of the Foo class, however this defeats the purpose of Guice since I’m not using it to initialize the class. Any dependency that the MessageProcessor has must be initialized outside of Guice
|
|
Instead, add @Inject to the MessageProcessor class and use Guice to fully instantiate the class.
|
|
With the new approach, I can easily add or remove new constructor arguments without having to update several different points in the code.
Use @Singletons carefully
Imagine if you have a dependency graph that looks like this. We have a root class marked as @Singleton. Only one class was marked as a Singleton, but the Jackson ObjectMapper at the bottom is not marked as Singleton. The Jackson ObjectMapper class is notoriously expensive to construct and has frequently caused massive latency issues in services because they don’t cache it.
MyRootClass was marked as @Singleton because the developer knew it had classes that were expensive and they didn’t want to redundantly create classes.
Problem: If another class refers to MyOtherClass or reuses the Guice module or that binding for other use cases, the ObjectMapper isn’t marked as Singleton. Thus we’d accidentally re-instantiate it each time.
This is an example of a developer poorly communicating their desires through code. If a class needs to be a Singleton, then that binding itself should be marked as
Solution: Don’t depend on your root classes to be marked as @Singleton. Instead use @Singleton on any class that needs to be a Singleton. Don’t mark it on every class, just the ones that care. Guice will automatically figure out which classes need to be recreated and which ones will be instantiated. That way you can re-use shared Guice modules across multiple systems.
Example
|
|
See Also: Use the Prod Stage
Use the Prod Stage when your service is deployed
Guice may eager or lazily initialize the Singleton depending on what stage (ie. PRODUCTION or DEVELOPMENT) the Injector was created using. See here for details.
Lazily initialized Singletons can very easily cause a high latency for the initial few requests to the service until Guice initializes all instances. Instead, you want to eagerly initialize all instances. Since this generally happens before the service added to a load balancer you can take as much time as needed to initialize singletons. This also has the benefit of the JVM preloading most of your code base.
Example on how to initialize Guice with eagerly initialized singletons:
|
|
Prefer bind() over @Provides or Providers
The AbstractModule.bind() method is only one line of code compared to @Provides or a Provider. It’s far more concise and doesn’t require changes if you add or remove parameters in your constructor.
Only define a @Provides when you specifically have complex initialization logic that can’t be handled by Guice. Creating them increases the amount of boilerplate code that your application includes with no improvement in code readability.
Bad:
|
|
Better:
|
|
Don’t add @Inject to a @Provides method
Don’t add @Inject to your @Provides methods. This is entirely meaningless. @Inject defines what constructor on a class Guice will use to construct or what fields Guice should inject post instantiating. It does not affect anything when defined on a @Provides or when defined on a class
Bad:
|
|
Bad:
|
|
Better:
|
|
|
|
Even Simpler:
|
|
Avoid @Inject on private fields
Avoid setting @Inject on fields unless you’re defining optional fields with default values (e.g. configuration values.)
Fields with @Inject on them…
- can’t be initialized in unit tests without using Guice to construct them. Developers may forget about this and try to instantiate it with new Example() and will be surprised by a NullPointerException later.
- are initialized after the constructor runs which means you have “two initialization phases”. This is often unexpected by other developers who expect the constructor to have all the values needed
Bad:
|
|
Better:
|
|
See also: Lombok AllArgsConstructor
Use Lombok’s AllArgsConstructor to reduce boilerplate
Lombok can reduce the amount of boilerplate in your code if you wish to use it. Here’s how you can use Lombok to create your constructor.
|
|
And add to your lombok.config. Without this, the @Named annotation on fields won’t be copied to the constructor. This will mean Guice will try to bind a generic value instead of your @Named() value
lombok.copyableAnnotations += javax.inject.Named
Unit Tests
Why would you want to write unit tests for your Guice modules and code?
What does it mean to unit test Guice modules and bindings? What should you test and not test? Here are some examples of what not to do.
The following example unit test directly calls the @Provides methods on a Module. Notice how there’s 3 different unit tests for a single @Provides methods, but this has several issues:
|
|
- It’s testing passing nulls into a @Provides method, but this is useless because Guice won’t pass nulls to your @Provides method (unless you specifically configure it to do so.) Instead, Guice itself going to throw an exception that says it can’t find the binding for your @Provides method. Thus you’re testing a path that will never happen.
- Every @Provides method is tested independently requiring copy and pasted code to implement it. There’s a lot of work involved just to get code coverage.
- By independently testing methods, you’re not really testing to see if the entire dependency graph is valid. For example, what if you had a module like this:
- @Test(expected = NullPointerException.class) is extremely bad because an NPE can be thrown for reasons other than what you expected.
|
|
You could independently test each method, but then it’s still fail at runtime because there’s no binding for BazBoo, it’s actually called Bar. Thus, we’ve written a lot of code that adds brittle code, but minimal value add. Guice has much smarter validations than your own tests. Don’t repeat them.
Instead, the following would be better.
Key Improvements:
- We’re testing the root-level classes and Guice automatically calls any @Provides, Providers, or @Inject=annotated constructors for us validating correctness
- Using JUnit5’s @ParameterizedTest feature reduces the amount of test duplication we have
- We can mock out classes that can’t be tested
|
|
Binding only test cases
If the entire purpose of your Guice module is to make a call to an external service and get data for a binding, then mocking it out doesn’t do much.
Instead, you can do a binding only test case that verifies that all binds and providers are available and bound to working methods without actually calling any of the code. While it’s not testing code, it’s still extremely valuable test case because it can ensure that you’re not missing a @Provides or any other type of binding.
|
|
Conclusion
In this post, I provide several Guice anti-patterns that I’ve seen in practice across different teams and alternative approaches to avoid these anti-patterns.
For more information, see the Guice wiki page.