Setup and dependency injection
Service lifecycle
It is possible to define logic that runs on service instance start up and shut down.
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 Bootstrap implements ServiceSetup {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final ComponentClient componentClient;
public Bootstrap(ComponentClient componentClient) { (2)
this.componentClient = componentClient;
}
@Override
public void onStartup() { (3)
logger.info("Service starting up");
var result = componentClient.forEventSourcedEntity("123").method(Counter::get).invoke();
logger.info("Initial value for entity 123 is [{}]", result);
}
@Override
public void onShutdown() { (4)
logger.info("Service shutting down");
}
| 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 |
| 4 | onShutdown is invoked when the service instance is shutting down, after it has stopped handling requests |
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. The same applies to onShutdown: it runs on each
instance as it shuts down, not once for the service as a whole.
onShutdown runs after the instance has stopped accepting requests and any in-flight requests have completed,
which makes it a good place to release resources allocated in the ServiceSetup constructor or in onStartup —
for example, closing a connection pool. Exceptions thrown from onShutdown are logged but do not block subsequent
shutdown steps. Shutdown waits for onShutdown at most 5 seconds and then continues without it, so keep the work
in it short.
Handling uncaught exceptions
When user code in a component or endpoint throws an exception that is not turned into a CommandException, a deliberate HTTP error response, or for gRPC endpoints, a akka.grpc.GrpcServiceException, the runtime catches it, logs it with a correlation id, and returns a generic 500 (or INTERNAL gRPC status) response carrying that correlation id to the client.
A service can be notified about each such exception by making the ServiceSetup class also implement akka.javasdk.UnhandledExceptionHandler. The typical use is forwarding the exception to an external error tracker such as Sentry.
@Setup
public class Bootstrap implements ServiceSetup, UnhandledExceptionHandler {
@Override
public void onUnhandledException(UnhandledExceptionContext context) {
Sentry.captureException(context.throwable());
}
}
The UnhandledExceptionContext passed to the callback exposes:
-
throwable()— the original exception with full stack trace and causes. -
correlationId()— the id surfaced to the client and present in the runtime log MDC at the time the exception was caught, useful for joining the error tracker event with runtime logs. -
subjectId()— the entity, workflow or agent id when the exception originated in a stateful component; empty for endpoints and other stateless components. -
componentId()— the value of@ComponentIdfor components that declare one, or the simple class name for endpoints. -
componentClassName()— the fully-qualified class name of the user component the exception originated in.
The callback is invoked once per caught exception on the service instance that handled the failing request, so in a service running multiple distributed instances each instance reports its own failures.
Disabling components
You can use ServiceSetup to disable components by overriding disabledComponents and returning a set of component classes to disable.
@Setup
public class Bootstrap implements ServiceSetup {
private final Config appConfig;
public Bootstrap(Config appConfig) {
this.appConfig = appConfig;
}
@Override
public Set<Class<?>> disabledComponents() { (1)
if (appConfig.getString("my-app.environment").equals("prod")) {
return Set.of(MyComponent.class); (2)
} else {
return Set.of(); (2)
}
}
}
| 1 | Override disabledComponents |
| 2 | Provide a set of component classes to disable depending on the configuration |
Using components from libraries
A service can use components from a library jar. The service only loads library components that are listed in its configuration.
Creating a component library
Create the library as a Maven project with the Akka parent pom, like a service. Set the non-akka-service property to true:
<properties>
<non-akka-service>true</non-akka-service>
</properties>
With this property, the build validates the components but does not build a container image. The build marks the component descriptor of the library as a library descriptor. In a multi-module build, set the property in each module that is a library.
The tests of the library load the components of the library itself, like the tests of a service.
Loading library components
Add the library as a dependency of the service. Then list the fully qualified class names of the components to load in application.conf, under the key for the component type they implement:
akka.javasdk.library.components {
http-endpoint = ["com.example.library.OrderEndpoint", "com.example.library.CustomerEndpoint"]
}
The service loads the listed classes on startup as if they were defined in the service itself, and logs the added library components.
List every library component that the service uses, including the components that a listed component calls. For example, list the entity that a listed endpoint calls. A call to a component that is not listed fails. Do not list SDK components, such as SessionMemoryEntity. The SDK loads them when the service needs them.
If the service tests use a configuration file other than application.conf, include application.conf from that file, or list the components in it too.
The service logs a warning on startup when a library contains components and the configuration lists no library components. The warning lists the components it found and the configuration that loads them. If some library components are listed, the service logs the components that are not listed. The service ignores a service setup class in a library and logs a warning about it. For a service that uses a library only for its other classes, log these messages at debug level:
akka.javasdk.library.log-unlisted-components = false
The configuration block has one key per component type:
| Component type | Config key |
|---|---|
Event Sourced Entity |
|
Key Value Entity |
|
Workflow |
|
View |
|
Consumer |
|
Timed Action |
|
Agent |
|
Autonomous Agent |
|
HTTP Endpoint |
|
gRPC Endpoint |
|
MCP Endpoint |
|
Evaluator |
|
Durable Evaluator |
|
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 all component types:
| Injectable class | Description |
|---|---|
|
Contains information about all agents, see Agents |
|
Access the user defined configuration picked up from |
|
Allows creating custom Open Telemetry metrics, see Metrics |
The following types can be injected in Service Setup, HTTP Endpoints, gRPC Endpoints, Agents, Consumers, Timed Actions, Workflows, and Evaluators:
| Injectable class | Description |
|---|---|
|
For interaction between components, see Component and service calls |
|
For storing and retrieving binary objects in named buckets, see Object storage |
|
For creating clients to make calls between Akka services and also to other HTTP servers, see Component and service calls |
|
For creating clients to make calls between Akka services and also to other gRPC servers, see Component and service calls |
|
For scheduling timed actions, see Timers |
|
Used for running Akka streams |
|
Utility for retrying calls |
|
An executor which runs each task in a virtual thread, and is safe to use for blocking async work, for example with |
|
For fetching recorded agent interactions from the ledger, primarily for evaluators, see Evaluators |
|
For invoking a configured classifier by name, see Classifiers |
|
For masking text with a configured sanitizer by name, see Data sanitization |
Furthermore, the following component specific types can also be injected:
| Component Type | Injectable classes |
|---|---|
Agent |
|
Endpoint |
|
Workflow |
|
Event Sourced Entity |
|
Key Value Entity |
|
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 Bootstrap implements ServiceSetup {
private final Config appConfig;
public Bootstrap(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 Bootstrap 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.
Configuration
Configuration properties for the service, or adjustments to the Akka default configuration, can be defined in src/main/resources/application.conf in HOCON format.
my-app {
some-feature-flag = true
environment = "test"
environment = ${?ENVIRONMENT}
}
akka.javasdk {
agent {
model-provider = openai
openai {
model-name = "gpt-4o-mini"
api-key = ${?OPENAI_API_KEY}
}
}
# dev-mode configuration is only used when running locally
dev-mode {
http-port = 9001
acl.enabled = false
}
}
${?ENVIRONMENT} and ${?OPENAI_API_KEY} means that if an environment variable is defined with the given name it will override the configuration property.
To access the configuration in application code you can use a constructor parameter com.typesafe.config.Config in all components and the ServiceSetup class. An example of this is shown in Disabling components.
Do not use ConfigFactory.load() since that will not load the application.conf as you intended. Use dependency injection of Config instead.
|
Test configuration
Test that are using the TestKitSupport are loading configuration from src/test/resources/application-test.conf if that exists, otherwise from application.conf.
include "application.conf"
my-app {
some-feature-flag = false
}
# application.conf declares backoffice services for the snippets. Resolving them needs the Akka
# platform, which a test does not have.
akka.javasdk.dev-mode.backoffice.enabled = false
Alternatively, the configuration of the test can be overridden in the testKitSettings:
public class ConfigIntegrationTest extends TestKitSupport {
@Override
protected TestKit.Settings testKitSettings() {
return TestKit.Settings.DEFAULT.withAdditionalConfig(
"""
akka.javasdk.agent.openai.api-key = n/a
"""
);
}
}
Reference configuration
The complete default configuration for the Akka SDK is presented on Service configuration.
In addition, there is also AI model provider configuration.