Component and service calls
An Akka service comprises many components. These components might depend on one another, on other Akka services, or even external services. This section describes how to call other components and services from within an Akka service.
Calling Akka components
Since Akka is an auto-scaling solution, components run distributed across many nodes within the same service. That’s why calls between Akka components are done via a client rather than through regular method calls. The receiving component instance may be on the same node, but it may also be on a different node.
Requests and responses are always serialized to JSON during transmission between the client and the component.
Component Client
The akka.javasdk.client.ComponentClient
is a utility for making type-safe calls between components within an Akka service. To use the ComponentClient
, you need to inject it into your component via the constructor:
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.INTERNET))
@HttpEndpoint("/counter")
public class CounterEndpoint {
private final ComponentClient componentClient;
public CounterEndpoint(ComponentClient componentClient) { (1)
this.componentClient = componentClient;
}
@Get("/{counterId}")
public Integer get(String counterId) {
return componentClient
.forEventSourcedEntity(counterId) (2)
.method(CounterEntity::get)
.invoke(); (3)
}
@Post("/{counterId}/increase/{value}")
public HttpResponse increase(String counterId, Integer value) {
componentClient
.forEventSourcedEntity(counterId)
.method(CounterEntity::increase)
.invoke(value);
return ok(); (4)
}
}
1 | Accept the ComponentClient as a constructor argument and keep it in a field. |
2 | Use a specific request builder for the component you want to call. |
3 | Invoking the method returns the T that the component eventually returns. |
4 | Adapt the response rather than returning it as is. In this case, you discard the response value and respond OK without a response body. |
The component client can call command handlers on Event Sourced Entities, Key Value Entities, Workflows, Timed Actions, and query methods on Views.
The component client is available for injection only in Service Setup, Agents, Endpoints, Consumers, Timed Actions, and Workflows. For more information, see dependency injection.
- NOTE
-
For component client error handling, see Errors and failures section.
Asynchronous execution
For the vast majority of your Akka programming tasks, writing clean and simple synchronous code is all you need.
One way to think about your synchronous code is that it returns already completed futures. For example, you could rewrite the first "hello world" sample to return a Java future, e.g. CompletionStage<String>
as follows:
@Get("/hello")
public CompletionStage<String> hello() {
return CompletableFuture.completedFuture("Hello world");
}
Obviously, just writing simple synchronous code is easier to read and maintain. If you need to make a component client call that is explicitly asynchronous, you can use the component client’s invokeAsync()
method, which returns a CompletionStage<T>
. This allows you to trigger multiple calls concurrently, enabling parallel processing.
public record IncreaseAllThese(List<String> counterIds, Integer value) {}
@Post("/increase-multiple")
public HttpResponse increaseMultiple(IncreaseAllThese increaseAllThese) throws Exception {
var triggeredTasks = increaseAllThese
.counterIds()
.stream()
.map(
counterId ->
componentClient
.forEventSourcedEntity(counterId)
.method(CounterEntity::increase)
.invokeAsync(increaseAllThese.value)
) (1)
.toList();
for (var task : triggeredTasks) {
task.toCompletableFuture().get(); (2)
}
return ok(); (3)
}
1 | Call invokeAsync() and collect each CompletionStage<T> . |
2 | When all tasks have been started, wait for all tasks to complete. |
3 | When all tasks have successfully completed, we can respond. |
Synchronous vs asynchronous component invocation
You decide how the component client invokes the component, and the Akka runtime handles the request in the background. The following table summarizes the key differences between synchronous and asynchronous component invocation.
Synchronous | Asynchronous | |
---|---|---|
When the component method returns |
After the method finishes |
Immediately |
Client behavior |
Waits for the result before continuing |
Continues immediately, must handle the result later |
Return type |
Whatever the component method returns directly |
A |
Component execution |
Always runs in the background |
Always runs in the background |
Common use case |
Calling a method and using the result in the next line of code |
Starting multiple async tasks or implementing background, always-on processes (Ambient AI) |
Ideal for |
Simple flows where the result is needed immediately |
Parallel task execution, deferred response handling, or long-running background logic |
When in doubt, write synchronous code
Trust that Akka will do the right thing and that the runtime makes the necessary optimizations.
If you do need explicit control over futures, such as creating streams from asynchronous sources or explicitly performing parallel work, then the component client’s invokeAsync()
and the full power of Java concurrency is there for you when you need it.
Calling Akka services
Calling other Akka services within the same project is done by invoking them using an HTTP or a gRPC client, depending on what type of endpoints the service provides.
Calling Akka services over HTTP
The service is identified by the name it has been deployed with. Akka takes care of routing requests to the service and keeping the data safe by encrypting the connection and handling authentication for you.
In the following snippet, we have an endpoint component that calls another service named counter
. It makes use of SDK-provided akka.javasdk.http.HttpClientProvider
which returns HTTP client instances for calling other Akka services.
In our delegating service implementation:
@Acl(allow = @Acl.Matcher(service = "*"))
@HttpEndpoint
public class DelegatingServiceEndpoint {
private final HttpClient httpClient;
public DelegatingServiceEndpoint(HttpClientProvider httpClient) { (1)
this.httpClient = httpClient.httpClientFor("counter"); (2)
}
// model for the JSON we accept
record IncreaseRequest(int increaseBy) {}
// model for the JSON the upstream service responds with
record Counter(int value) {}
@Post("/delegate/counter/{counterId}/increase")
public String addAndReturn(String counterId, IncreaseRequest request) {
var response = httpClient
.POST("/counter/" + counterId + "/increase") (3)
.withRequestBody(request)
.responseBodyAs(Counter.class)
.invoke(); (4)
if (response.status().isSuccess()) { (5)
return "New counter vaue: " + response.body().value;
} else {
throw new RuntimeException("Counter returned unexpected status: " + response.status());
}
}
}
1 | Accept a HttpClientProvider parameter for the constructor. |
2 | Use it to look up a client for the counter service. |
3 | Use the HttpClient to prepare a REST call to the counter service endpoint. |
4 | Invoking the call will return a StrictResponse<T> with details about the result as well as the deserialized response body. |
5 | Handle the response, which may be successful or an error. |
The HTTP client provider is only available for injection in the following types of components: HTTP Endpoints, gRPC Endpoints, Workflows, Consumers and Timed Actions. |
Calling external HTTP services
Calling external services deployed on different Akka projects or any other external HTTP server is also done with the HttpClientProvider
. Instead of a service name, the protocol and full server name are used when calling httpClientFor
. For example, https://example.com
or http://example.com
.
package com.example.callanotherservice;
import akka.javasdk.annotations.Acl;
import akka.javasdk.annotations.http.Get;
import akka.javasdk.annotations.http.HttpEndpoint;
import akka.javasdk.http.HttpClient;
import akka.javasdk.http.HttpClientProvider;
import akka.javasdk.http.StrictResponse;
import java.util.List;
import java.util.stream.Collectors;
@HttpEndpoint
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.ALL))
public class CallExternalServiceEndpoint {
private final HttpClient httpClient;
public record PeopleInSpace(List<Astronaut> people, int number, String message) {}
public record Astronaut(String craft, String name) {}
public record AstronautsResponse(List<String> astronautNames) {}
public CallExternalServiceEndpoint(HttpClientProvider httpClient) { (1)
this.httpClient = httpClient.httpClientFor("http://api.open-notify.org"); (2)
}
@Get("/iss-astronauts")
public AstronautsResponse issAstronauts() {
StrictResponse<PeopleInSpace> peopleInSpaceResponse = httpClient
.GET("/astros.json") (3)
.responseBodyAs(PeopleInSpace.class) (4)
.invoke();
var astronautNames = peopleInSpaceResponse
.body()
.people.stream() (5)
.filter(astronaut -> astronaut.craft.equals("ISS"))
.map(astronaut -> astronaut.name)
.collect(Collectors.toList());
return new AstronautsResponse(astronautNames); (6)
}
}
1 | Accept a HttpClientProvider parameter for the constructor. |
2 | Look up a HttpClient for a service using http protocol and server name. |
3 | Issue a GET call to the path /astros.json on the server. |
4 | Specify a class to parse the response body into. |
5 | Once the call completes, handle the response. |
6 | Return an adapted result object which will be turned into a JSON response. |
Calling Akka services over gRPC
The service is identified by the name it has been deployed with. Akka takes care of routing requests to the service and keeping the data safe by encrypting the connection and handling authentication for you.
In this sample we will implement a gRPC endpoint that delegates a call to a gRPC endpoint of a customer registry service, deployed with the service name customer-registry
.
The SDK provides akka.javasdk.grpc.GrpcClientProvider
which provides gRPC client instances for calling other services.
To consume an external gRPC service, that service’s protobuf descriptor must be added to the src/proto
directory of the project. This
triggers generation of a client interface and Java classes for all the message types used as requests and responses for
methods in that service.
Since the service protobuf descriptors need to be shared between the provider service and the consuming service, one simple option is to copy the service descriptions to each service that needs them. It is also possible to use a shared library with the protobuf descriptors. |
In our delegating service implementation:
@GrpcEndpoint
public class DelegateCustomerGrpcEndpointImpl implements DelegateCustomerGrpcEndpoint {
private final Logger log = LoggerFactory.getLogger(getClass());
private CustomerGrpcEndpointClient customerService;
public DelegateCustomerGrpcEndpointImpl(GrpcClientProvider clientProvider) { (1)
customerService = clientProvider.grpcClientFor(
CustomerGrpcEndpointClient.class,
"customer-registry"
); (2)
}
@Override
public CreateCustomerResponse createCustomer(CreateCustomerRequest in) {
log.info("Delegating customer creation to upstream gRPC service: {}", in);
if (in.getCustomerId().isEmpty()) throw new GrpcServiceException(
Status.INVALID_ARGUMENT.augmentDescription("No id specified")
);
try {
return customerService.createCustomer(in); (3)
} catch (Exception ex) {
throw new RuntimeException("Delegate call to create upstream customer failed", ex);
}
}
}
1 | Accept a GrpcClientProvider parameter for the constructor. |
2 | Use the generated gRPC client interface for the service (CustomerGrpcEndpointClient.class ) and the service name (customer-registry ) to look up a client. |
3 | Use the client to call the other service and return a CompletionStage<CreateCustomerResponse> . |
Since the called service and the DelegateCustomerGrpcEndpoint
share the same request and response protocol, no further transformation
of the request or response is needed here.
For dev mode and in tests, providing a config override in application.conf
like for external calls is possible, however
when deployed such configuration is ignored.
The gRPC client provider is only available for injection in the following types of components: HTTP Endpoints, gRPC endpoints, Workflows, Consumers and Timed Actions. |
Calling external gRPC services
Calling gRPC services deployed on different Akka projects or any other external gRPC server is also done with the GrpcClientProvider
. Instead of a service name, the protocol and the fully qualified DNS name of the service are used when calling grpcClientFor
. For example hellogrpc.example.com
.
@GrpcEndpoint
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.ALL))
public class CallExternalGrpcEndpointImpl implements CallExternalGrpcEndpoint {
private final ExampleGrpcEndpointClient external;
public CallExternalGrpcEndpointImpl(GrpcClientProvider clientProvider) { (1)
external = clientProvider.grpcClientFor(
ExampleGrpcEndpointClient.class,
"hellogrpc.example.com"
); (2)
}
@Override
public HelloReply callExternalService(HelloRequest in) {
return external.sayHello(in); (3)
}
}
1 | Accept a GrpcClientProvider parameter for the constructor. |
2 | Use the generated gRPC client interface for the service (ExampleGrpcEndpointClient.class ) and the service name (doc-snippets ) to look up a client. |
3 | Use the client to call the other service and return a CompletionStage<HelloReply> . |
Since the called service and the DelegatingGrpcEndpoint
share the same request and response protocol, no further transformation
of the request or response is needed here.
The service is expected to accept HTTPS connections and run on the standard HTTPS port (443). For calling a service on a nonstandard
port, or served unencrypted (not recommended) it is possible to define configuration overrides in application.conf
(or application-test.conf
specifically for tests):
akka.javasdk.grpc.client."hellogrpc.example.com" {
# configure external call, to call back to self
host = "localhost"
port = 9000
use-tls = false
}