Setup and dependency injection

Service lifecycle

It is possible to define logic that runs on service instance start up.

This is done by creating a class implementing akka.javasdk.ServiceSetup and annotating it with akka.javasdk.annotations.Setup. Only one such class may exist in the same service.

@Setup (1)
public class CounterSetup implements ServiceSetup {

  private  final Logger logger = LoggerFactory.getLogger(getClass());
  private final ComponentClient componentClient;

  public CounterSetup(ComponentClient componentClient) { (2)
    this.componentClient = componentClient;
  }

  @Override
  public void onStartup() { (3)
    logger.info("Service starting up");
    componentClient.forEventSourcedEntity("123")
        .method(Counter::get)
        .invokeAsync().thenAccept(result ->
          logger.info("Initial value for entity 123 is [{}]", result)
        );
  }
1 One annotated implementation of ServiceSetup
2 A few different objects can be dependency injected, see below
3 onStartup is invoked at service start, but before the service is completely started up

It is important to remember that an Akka service consists of one to many distributed instances that can be restarted individually and independently, for example during a rolling upgrade. Each such instance starting up will invoke onStartup when starting up, even if other instances run it before.

Dependency injection

The Akka SDK provides injection of types related to capabilities the SDK provides to components.

Injection is done as constructor parameters for the component implementation class.

The following types can be injected in Service Setup, Endpoints, Consumers, Timed Actions and Workflow components:

Injectable class Description

akka.javasdk.client.ComponentClient

for interaction between components, see Component and service calls

akka.javasdk.http.HttpClientProvider

for creating clients to make calls between Akka services and also to other HTTP servers, see Component and service calls

akka.javasdk.timer.TimerScheduler

for scheduling timed actions, see Timed Actions

akka.stream.Materializer

Used for running Akka streams

com.typesafe.config.Config

Access the user defined configuration picked up from application.conf

Furthermore, the following component type specific types can also be injected:

Component Type Injectable classes

Endpoint

io.opentelemetry.api.trace.Span for creating custom traces

Workflow

akka.javasdk.workflow.WorkflowContext for access to the workflow id

Event Sourced Entity

akka.javasdk.eventsourcedentity.EventSourcedEntityContext for access to the entity id

Key Value Entity

akka.javasdk.keyvalueentity.KeyValueEntityContext for access to the entity id

Custom dependency injection

In addition to the predefined objects a service can also provide its own objects for injection. Any unknown types in component constructor parameter lists will be looked up using a DependencyProvider.

Providing custom objects for injection is done by implementing a service setup class with an overridden createDependencyProvider that returns a custom instance of akka.javasdk.DependencyProvider. A single instance of the provider is used for the entire service instance.

Note that the objects returned from a custom DependencyProvider must either be a new instance for every call to the dependency provider or be thread safe since they will be shared by any component instance accepting them, potentially each running in parallel. This is best done by using immutable objects which is completely safe.

Injecting shared objects that use regular JVM concurrency primitives such as locks, can easily block individual component instances from running in parallel and cause throughput issues or even worse, deadlocks, so should be avoided.

The implementation can be pure Java without any dependencies:

@Setup
public class MyAppSetup implements ServiceSetup {

  private final Config appConfig;

  public MyAppSetup(Config appConfig) {
    this.appConfig = appConfig;
  }

  @Override
  public DependencyProvider createDependencyProvider() { (1)
    final var myAppSettings =
        new MyAppSettings(appConfig.getBoolean("my-app.some-feature-flag")); (2)

    return new DependencyProvider() { (3)
      @Override
      public <T> T getDependency(Class<T> clazz) {
        if (clazz == MyAppSettings.class) {
          return (T) myAppSettings;
        } else {
          throw new RuntimeException("No such dependency found: "+ clazz);
        }
      }
    };
  }
1 Override createDependencyProvider
2 Create an object for injection, in this case an immutable settings class built from config defined in the application.conf file of the service.
3 Return an implementation of DependencyProvider that will return the instance if called with its class.

It is now possible to declare a constructor parameter in any component accepting MyAppSettings. The SDK will inject the instance provided by the DependencyProvider.

Or make use of an existing dependency injection framework, like this example leveraging Spring:

public class CounterSetup implements ServiceSetup {

  @Override
  public DependencyProvider createDependencyProvider() {
    try {
      AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); (1)
      ResourcePropertySource resourcePropertySource = new ResourcePropertySource(new ClassPathResource("application.properties"));
      context.getEnvironment().getPropertySources().addFirst(resourcePropertySource);
      context.registerBean(ComponentClient.class, () -> componentClient);
      context.scan("com.example");
      context.refresh();
      return context::getBean; (2)
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }


}
1 Set up a Spring AnnotationConfigApplicationContext
2 DependencyProvider is a SAM (single abstract method) type with signature Class<T> → T, the method reference AnnotationConfigApplicationContext#getBean matches it.

Custom dependency injection in tests

The TestKit allows providing a custom DependencyProvider through TestKit.Settings#withDependencyProvider(provider) so that mock instances of dependencies can be used in tests.

public class MyIntegrationTest extends TestKitSupport {

  private static final DependencyProvider mockDependencyProvider = new DependencyProvider() { (1)
    @SuppressWarnings("unchecked")
    @Override
    public <T> T getDependency(Class<T> clazz) {
      if (clazz.equals(MyAppSettings.class)) {
           return (T) new MyAppSettings(true);
      } else {
        throw new IllegalArgumentException("Unknown dependency type: " + clazz);
      }
    }
  };

  @Override
  protected TestKit.Settings testKitSettings() {
    return TestKit.Settings.DEFAULT
        .withDependencyProvider(mockDependencyProvider); (2)
  }
1 Implement a test specific DependencyProvider.
2 Configure the TestKit to use it.

Any component injection happening during the test will now use the custom DependencyProvider.

The test specific DependencyProvider must be able to provide all custom dependencies used by all components that the test interacts with.