DEV Community

Cover image for A Practical Guide to Google Guice
İbrahim Gündüz
İbrahim Gündüz

Posted on • Originally published at Medium

A Practical Guide to Google Guice

Google Guice is a lightweight dependency injection (DI) framework that simplifies development by letting you manage object creation without pulling in large, unnecessary framework dependencies.

The framework focuses on object creation and wiring, but it can do much more through its extensions—such as servlet integration (guice-servlet), JPA persistence support (guice-persist or guice-persist-jpa), and even Spring integration (guice-spring).

In this article, we'll focus on the core features of the framework that enable us to accomplish common development tasks.

Setup

To start using Google Guice, add the following Maven dependency to your project

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>7.0.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Constructor Injection

Google Guice allows you to inject dependencies using constructors or setter methods without changing your implementation logic. You can inject a dependency simply by adding the @Inject annotation to the class constructor and defining the appropriate bindings in your module class.

Let's take a look at the following example:

package com.example;

import com.google.inject.Inject;
import java.math.BigDecimal;

public class CardPaymentProcessor implements PaymentProcessor {
    private final CardVault cardVault;
    private final PaymentProvider paymentProvider;
    private final TransactionStorage transactionStorage;

    @Inject
    public CardPaymentProcessor(CardVault cardVault, PaymentProvider paymentProvider, TransactionStorage transactionStorage) {
        this.cardVault = cardVault;
        this.paymentProvider = paymentProvider;
        this.transactionStorage = transactionStorage;
    }

    public PaymentReceipt processPayment(String cardToken, BigDecimal paymentAmount) {
        try {
            CardData cardData = cardVault.getCardData(cardToken);
            PaymentResult paymentResult = paymentProvider.charge(cardData, paymentAmount);
            transactionStorage.savePaymentResult(paymentResult);

            return paymentResult.isSuccessful() ?
                    PaymentReceipt.forSuccess(paymentResult, paymentAmount) :
                    PaymentReceipt.forFailure(paymentResult, paymentAmount);
        } catch (PaymentProcessingException exception) {
            return PaymentReceipt.forException(exception, paymentAmount);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see in the example above, the CardPaymentProcessor class depends on several other components through their interfaces. Since Google Guice resolves dependencies based on interface bindings, it provides greater flexibility and makes it easy to replace these implementations with mocks during testing—just like other modern dependency injection frameworks.

As a next step, we need to define the bindings as shown below:

package com.example;

import com.google.inject.AbstractModule;

public class MainModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(PaymentProvider.class).to(HsbcPaymentProvider.class);
        bind(TransactionStorage.class).to(DatabaseTransactionStorage.class);
        bind(CardVault.class).to(ExternalCardVault.class);
        bind(PaymentProcessor.class).to(CardPaymentProcessor.class);
    }
}

Enter fullscreen mode Exit fullscreen mode

And finally, we can get an instance of PaymentProcessor implementation as shown below

public static void main(String[] args) {
    Injector injector = Guice.createInjector(new MainModule());

    PaymentProcessor paymentProcessor = injector.getInstance(PaymentProcessor.class);

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Setter Injection

You can inject a dependency by creating a setter method annotated with @Inject.

Continuing with the same example from above:

public class CardPaymentProcessor implements PaymentProcessor {
  // ...
  private PaymentProvider paymentProvider;
  // ...

  @Inject
  public void setPaymentProvider(PaymentProvider paymentProvider) {
    this.paymentProvider = paymentProvider;
  }
}
Enter fullscreen mode Exit fullscreen mode

Because Guice injects the paymentProvider dependency after the object is instantiated, the PaymentProvider field cannot be declared as final.

You can create the module class in the same way as shown in the Constructor Injection section.

Service Factory

Factories are used for objects that require a special creation process. Guice provides a similar mechanism called Provider to help you create and inject such objects while still maintaining clean dependency management.

Providers can either be separate classes that implement Guice’s Provider interface or methods annotated with @Provides that create and return the desired object.

Let's start by creating the provider class.

package com.example;

import com.google.inject.Provider;

import java.net.http.HttpClient;
import java.time.Duration;

public class HttpClientProvider implements Provider<HttpClient> {
    private final static int CONNECTION_TIMEOUT_IN_MS = 10_000;
    @Override
    public HttpClient get() {
        return HttpClient.newBuilder()
                .connectTimeout(Duration.ofMillis(CONNECTION_TIMEOUT_IN_MS))
                .followRedirects(HttpClient.Redirect.ALWAYS)
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, it's a well-known factory class. In order tell Guice how to create this dependency, we need to define a binding as shown below.

@Override
protected void configure() {
    bind(HttpClient.class)
        .toProvider(HttpClientProvider.class)
        .in(Scopes.SINGLETON);
}
Enter fullscreen mode Exit fullscreen mode

And you can inject it either through constructor or setter methods.

package com.example;

import com.google.inject.Inject;

import java.net.http.HttpClient;

public class LoggingService {
    private final HttpClient httpClient;

    @Inject
    public LoggingService(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public void info(String message) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

You can access the LogginService instance in the same way as shown the examples above. But I want to touch on something here:

public static void main(String[] args) {
        Injector injector = Guice.createInjector(new MainModule());
        LoggingService loggingService = injector.getInstance(LoggingService.class);
        loggingService.info("Test message");
    }
Enter fullscreen mode Exit fullscreen mode

Normally, we don’t create interfaces for every single class. For example, I didn’t create an interface for LoggingService. When we request an instance of a concrete class by its type, Guice can construct it automatically, without requiring any additional bindings to determine which implementation to use.

Another topic worth mentioning is the use of scopes. In our example, we defined a binding with a specific scope using .in(Scopes.SINGLETON). This is necessary because Guice creates a new instance of a dependency every time it is requested when no scope is specified. In other words, bindings are in prototype (no-scope) mode by default.

Working In Multi-Module Projects

As your project grows, maintaining all bindings in a single module can become difficult. Guice supports working with multiple modules, allowing you to organize your bindings across separate module classes.

public static void main(String[] args) {
    Injector injector = Guice.createInjector(
            new PaymentModule(),
            new BillingModule()
    );

    BillingService billingService = injector.getInstance(BillingService.class);
    billingService.charge("afafc9a3-346a-4dce-9909-ca65dc45fadc", BigDecimal.valueOf(100));
}
Enter fullscreen mode Exit fullscreen mode

The order in which modules are provided does not matter. Guice merges all bindings into a single injector regardless of the order.

Named Bindings

Sometimes, we may need multiple instances of the same class with different configurations. To tell Guice which version of the class should be injected, we can assign names to the bindings. Guice allows us to create distinct versions of a binding using named bindings

You can define bindings with a name as shown below:

package com.example;

import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MainModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Logger.class)
                .annotatedWith(Names.named("appLogger"))
                .toInstance(LoggerFactory.getLogger("application"));

        bind(Logger.class)
                .annotatedWith(Names.named("auditLogger"))
                .toInstance(LoggerFactory.getLogger("audit"));
    }
}
Enter fullscreen mode Exit fullscreen mode

And you can inject the desired instance by annotating the parameter with @Named annotation.

public class HsbcPaymentProvider implements PaymentProvider {
    private final Logger logger;

    @Inject
    public HsbcPaymentProvider(@Named("appLogger") Logger logger) {
        this.logger = logger;
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Loading Configurations From .properties Files

Guice enables you to bind and use configuration values stored in a .properties file, as shown below.

First, create an application.properties file under the resources/ directory:

datasource.url=jdbc:mysql://localhost:3306/app-database-dev
datasource.user=appuser
datasource.password=password
Enter fullscreen mode Exit fullscreen mode

Next, create a module that loads and binds these properties:

package com.example;

import com.google.inject.AbstractModule;
import com.google.inject.name.Names;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class ConfigurationModule extends AbstractModule {
    private static final String CONFIG_FILE = "application.properties";

    @Override
    protected void configure() {
        ClassLoader classLoader = getClass().getClassLoader();
        InputStream configFileStream = classLoader.getResourceAsStream(CONFIG_FILE);

        if (configFileStream == null) {
            throw new RuntimeException("Could not find " + CONFIG_FILE + " on the classpath");
        }

        try {
            Properties properties = new Properties();
            properties.load(configFileStream);
            Names.bindProperties(binder(), properties);
        } catch (IOException e) {
            throw new RuntimeException("Failed to load properties from " + CONFIG_FILE, e);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Now you can inject configuration values into your classes using the @Named annotation:

// ...
public class DatabaseTransactionStorage implements TransactionStorage {
    private final String datasourceUrl;
    private final String dbUser;
    private final String dbPassword;

    @Inject
    public DatabaseTransactionStorage(
            @Named("datasource.url") String datasourceUrl,
            @Named("datasource.username") String dbUser,
            @Named("datasource.password") String dbPassword) {
        this.datasourceUrl = datasourceUrl;
        this.dbUser = dbUser;
        this.dbPassword = dbPassword;
    }

    // ...
}

Enter fullscreen mode Exit fullscreen mode

You can also retrieve configuration values directly from the DI container:

public static void main(String[] args) {
    Injector injector = Guice.createInjector(new ConfigurationModule());
    String datasourceUrl = injector.getInstance(Key.get(String.class, Names.named("datasource.url")));
    System.out.println(datasourceUrl);
}
Enter fullscreen mode Exit fullscreen mode

Hope you enjoyed reading. As always, you can find the complete example in the following GitHub repository:

Thanks!

Credits:

Top comments (0)