Best Practices for working with Google Guice

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class SpecificFooClient implements FooClient {}

class FooModule {
  SpecificFooClient providesFooClient(ClassA a) {
    return SpecificFooClient(a);
  }
}

class ConsumingClass {
  @Inject
  public ConsumingClass(SpecificFooClient client) {}
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class FooModule {
  @Named("CACHING_FOO_CLIENT")
  FooClient providesFooClient(ClassA a) {
    return SpecificFooClient(a);
  }
}

// In another module
class FooModule {
  @Named("NON_CACHED_FOO_CLIENT")
  FooClient providesFooClient(ClassA a) {
    return SpecificFooClient(a);
  }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// In another module
class FooModule {
  FooClient providesFooClient(ClassA a) {
    if (isShortRunningJob()) {
       return LongCachedFooClient(a);
    } else {
       return TimeLimitedCacheFooClient(a);
    }
  }
}

Or create two separate Guice modules for each environment that return the correct type and avoid any runtime configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ShortRunningJobModule {
  FooClient providesFooClient(ClassA a) {
    return LongCachedFooClient(a);
  }
}

class LongRunningServiceModule {
  FooClient providesFooClient(ClassA a) {
    return TimeLimitedCacheFooClient(a);
  }
}

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

1
2
3
4
5
public void configure() {
  bind(MessageProcessor.class).toInstance(
     new MessageProcessor(this.sqsClientBuilder())
 );
}

Instead, add @Inject to the MessageProcessor class and use Guice to fully instantiate the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class MessageProcessor {
 @Inject
 MessageProcessor(SqsClientBuilder builder) {
 }
}

class MyModule extends AbstractModule {
  public void configure() {
    // This space intentionally left blank because Guice automatically 
    // knows how to initialize it when used
  }
}

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

1
2
3
4
5
6
7
8
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.inject.Singleton;

class SerializationModule extends AbstractModule {
   void configure() {
      bind(ObjectMapper.class).in(Singleton.class);
   }
}

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:

1
2
3
4
import com.google.inject.Injector;
import com.google.inject.Stage;

Injector injector = Injector.createInjector(Stage.PRODUCTION, new Module());

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class MyModule extends AbstractModule {
  @Provides
  FooBar providesFooBar(ClassA a, ClassB b, ClassC c, ClassD d) {
    return SpecificFooBar(a, b, c, d);
  }
}

class SpecificFooBar {
  SpecificFooBar(ClassA a, ClassB b, ClassC c, ClassD d) {
    // ...
  }
}

Better:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class MyModule {
  void configure() {
    bind(FooBar.class).to(SpecificFooBar.class);
  }
}

class SpecificFooBar {
  @Inject
  SpecificFooBar(ClassA a, ClassB b, ClassC c, ClassD d) {
    // ...
  }
}

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:

1
2
3
4
5
6
class MyModuleextends AbstractModule {
 @Provides
 @Inject
  public SomeClass provideSomeClass() {
 }
}

 Bad:

1
2
3
4
// Bad (This does nothing)
@Inject
class SomeClass {
}

Better:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class SomeClass {
 // OK
 @Inject
 private String someField;

 // OK
 @Inject
 public SomeClass(Another classFoo) {
 }
}
1
2
3
4
5
6
7
8
class SomeClass {
  private String someField;

  // OK
  @Inject
  public SomeClass(Another classFoo) {
  }
}

Even Simpler:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@AllArgsConstructor(onConstructor=@__(@Inject))
classSomeClass {
  // OK
  @Inject
  private String someField;

  @Inject
  public SomeClass(Another classFoo) {
  }
}

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:

1
2
3
4
class Example {
  @Inject
  private final SomeClass someClass;
}

Better:

1
2
3
4
5
6
7
8
class Example {
  private final SomeClass someClass;

  @Inject
  public Example(SomeClass someClass) {
    this.someClass = someClass;
  }
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import javax.inject.Named;
import javax.inject.Inject;

@AllArgsConstructor(onConstructor = @__({@Inject}))
class Example {
  private final SomeClass someClass;

  // An example with @Named
  @Named("MyTestString")
  private final String aTestString;
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test(expected = NullPointerException.class)
public void getServiceClient_withNullRegion_throwsException() {
    serviceClientModule.getClient(null);
}

@Test(expected = NullPointerException.class)
public void getServiceClient_withSystemEnvNotSet_throwsException() {
   // given
   ENVIRONMENT_VARIABLES.set(ServiceClientModule.SERVICE_AUTH_ROLE, null);

   // when
   serviceClientModule.getClient(REGION);
}

@Test
public void getServiceClient_withValidRegion_returnsValidClient() {
   // given

   // when
   final Interceptor interceptor = serviceClientModule.getAuthInterceptor(REGION);

   // then
   assertThat(interceptor, is(notNullValue()));
}
  1. 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.
  2. 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.
  3. 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:
  4. @Test(expected = NullPointerException.class) is extremely bad because an NPE can be thrown for reasons other than what you expected.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class BadModule extends AbstractModule {
  // ...

  @Provides
  public FooBar providesFooBar(BazBoo baz) {
    return new FooBar(baz);
  }

  @Provides
  public Bar providesBar() {
    // ...
  }
}

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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Use JUnit5
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.ParameterizedTest;

class ModuleTest {
 @ParameterizedTest
 @ValueSource(classes = {
    // Add your root-level classes here. Guice will automatically initialize the entire dependency graph behind the scenes and validate that all dependencies work correctly.
    SomeClient.class,
    AnotherClient.class
  })
  void canInitializeClients(Class someClass) {
    Injector injector = Guice.createInjector(binder -> {
      binder.install(new ModuleUnderTest()); // Install the module(s) you're testing
      // If you need to mock anything out because it can't be unit tested, then do this:
      binder.bind(AWSCredentialsProvider.class).toInstance(Mockito.mock(AWSCredentialsProvider.class));
    });

    // If your ModuleUnderTest provides objects you need to mock out, then you need to override:
    Injector injector = Guice.createInjector(
      Modules.override(new ModuleUnderTest())
      .with(binder -> {
        // If you need to mock anything out because it can't be unit tested, then do this:
        binder.bind(AWSCredentialsProvider.class).toInstance(Mockito.mock(AWSCredentialsProvider.class));
      }));

    Assertions.assertNotNull(injector.getInstance(someClass));
  }
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.ParameterizedTest;
import com.google.inject.Stage;

class ModuleTest {
  @ParameterizedTest
  @ValueSource(classes = {
    // Add your root-level classes here. Guice will automatically initialize the entire dependency graph behind the scenes and validate that all dependencies work correctly.
    SomeClient.class,
    AnotherClient.class
  })
  void canInitializeClients(Class someClass) {
    // Stage.TOOL means that no Providers actually run, but it's still sufficient to validate
    Injector injector = Guice.createInjector(Stage.TOOL, new ModuleUnderTest());

    Assertions.assertNotNull(injector.getBinding(someClass));
  }
}

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.

Copyright - All Rights Reserved

Comments

Comments are currently unavailable while I move to this new blog platform. To give feedback, send an email to adam [at] this website url.