Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ Down below is an exhaustive list of all the sample:
6. [Sequencing Policy](sequencing-policy/README.md) - Sample showing how to set up a custom `SequencingPolicy` to adjust
the event sequence for a `PooledStreamingEventProcessor`.
7. [Snapshots](snapshots/README.md) - Sample showing how to configure event-sourced entity snapshotting.
8. [Stateful Event Handler](stateful-event-handler/README.md) - Sample showing a stateful event handler that can be
8. [Spring Cloud Command Routing](spring-cloud-command-routing/README.md) - Sample showing how the Spring Cloud
connector routes commands between the instances of an application, without Axon Server.
9. [Stateful Event Handler](stateful-event-handler/README.md) - Sample showing a stateful event handler that can be
used as a replacement for sagas.
9. [Subscription Query - REST](subscription-query-rest/README.md) - Sample showing how to use Axon's subscription query
10. [Subscription Query - REST](subscription-query-rest/README.md) - Sample showing how to use Axon's subscription query
cleanly in a REST-based controller.
10. [Subscription Query - Streaming](subscription-query-streaming/README.md) - Sample showing how to use Axon's
11. [Subscription Query - Streaming](subscription-query-streaming/README.md) - Sample showing how to use Axon's
subscription query cleanly in a streaming-based controller.
11. [Workflow Saga](workflow-saga/README.md) - Sample showing a Saga-like process modelled with the AxoniQ Workflow
12. [Workflow Saga](workflow-saga/README.md) - Sample showing a Saga-like process modelled with the AxoniQ Workflow
Engine.

## Topics now covered by the Axon Framework project itself
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<module>serialization-avro</module>
<module>sequencing-policy</module>
<module>snapshots</module>
<module>spring-cloud-command-routing</module>
<module>stateful-event-handler</module>
<module>subscription-query-rest</module>
<module>subscription-query-streaming</module>
Expand Down
61 changes: 61 additions & 0 deletions spring-cloud-command-routing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Spring Cloud Command Routing

This sample shows how the Axoniq Framework Spring Cloud connector distributes commands between the instances of an
application, without Axon Server in between.

Every instance runs the same application. It registers with a Spring Cloud discovery service (Consul, here) and
handles one command, `SayHello`, answering with the id of the instance that handled it:

```
Hello alice, from spring-cloud-command-routing-8082
```

Start several instances and send requests to any of them, and you can see how commands are routed:

1. `SayHello` declares `@Command(routingKey = "name")`. The connector hashes the routing key onto a ring of all
instances that registered with discovery, so every greeting for the same name is handled by the same instance, no
matter which instance received the request.
2. Different names are spread over the instances.
3. When an instance stops, only the names it handled move to another instance. The others stay where they were.

There is no configuration specific to Axon: adding `axoniq-springcloud` next to a Spring Cloud discovery
implementation, such as `spring-cloud-starter-consul-discovery`, and a servlet web stack
(`spring-boot-starter-web`), is enough. Instances call each other over HTTP, on the port they registered with.

### Running the application

1. Start Consul. A `docker-compose.yml` is provided for this:

```bash
docker compose up -d
```

2. Start three instances of the application, each on its own port, in separate terminals:

```bash
PORT=8081 ../mvnw spring-boot:run
PORT=8082 ../mvnw spring-boot:run
PORT=8083 ../mvnw spring-boot:run
```

The Consul UI at [http://localhost:8500](http://localhost:8500) lists the instances as they register.

3. Send greetings through different instances, using the `requests.http` file or `curl`:

```bash
for port in 8081 8082 8083; do curl localhost:$port/hello/alice; echo; done
for name in alice bob carol dave erin; do curl localhost:8081/hello/$name; echo; done
```

The first loop answers from the same instance three times. The second shows names spread over the instances.

4. Stop one of the instances, wait a few seconds for discovery to notice, and send the same greetings again.

### Spring Boot 3

The sample builds against Spring Boot 4 and Spring Cloud 2025.1 by default. To run it against Spring Boot 3.5 and
Spring Cloud 2025.0 instead, enable the `spring-boot-3` profile:

```bash
PORT=8081 ../mvnw -Pspring-boot-3 spring-boot:run
```
5 changes: 5 additions & 0 deletions spring-cloud-command-routing/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
consul:
image: hashicorp/consul
ports:
- '8500:8500'
82 changes: 82 additions & 0 deletions spring-cloud-command-routing/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.axoniq</groupId>
<artifactId>code-samples</artifactId>
<version>0.0.2-SNAPSHOT</version>
</parent>

<artifactId>spring-cloud-command-routing</artifactId>

<name>Spring Cloud Command Routing</name>
<description>
Sample application showing how the Axoniq Framework Spring Cloud connector routes commands between the
instances of an application, without Axon Server.
</description>

<properties>
<!-- The Spring Cloud connector ships with Axoniq Framework 5.4 -->
<axoniq-framework.version>5.4.0-SNAPSHOT</axoniq-framework.version>
<!-- Spring Cloud 2025.1 (Oakwood) matches Spring Boot 4. The spring-boot-3 profile switches both. -->
<spring-cloud.version>2025.1.3</spring-cloud.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Axon -->
<dependency>
<groupId>io.axoniq.framework</groupId>
<artifactId>axoniq-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.axoniq.framework</groupId>
<artifactId>axoniq-springcloud</artifactId>
</dependency>
<!-- Spring: members receive commands through Spring MVC, and find each other through Consul -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>

<profiles>
<profile>
<!-- Builds and runs the same sample on Spring Boot 3.5 with Spring Cloud 2025.0 (Northfields). -->
<id>spring-boot-3</id>
<properties>
<spring-boot.version>3.5.16</spring-boot.version>
<spring-cloud.version>2025.0.3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Axon Framework uses Jackson 3, which needs newer Jackson 2 annotations than Boot 3.5 manages -->
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.22.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
</profiles>
</project>
11 changes: 11 additions & 0 deletions spring-cloud-command-routing/requests.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
### Greet alice through the first instance

GET http://localhost:8081/hello/alice

### Greet alice through the second instance: the same instance answers

GET http://localhost:8082/hello/alice

### Greet bob through the first instance: possibly another instance answers

GET http://localhost:8081/hello/bob
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.axoniq.springcloudcommandrouting;

import org.axonframework.messaging.commandhandling.annotation.CommandHandler;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.stereotype.Component;

/**
* Answers {@link SayHello} with a greeting naming the instance that handled it.
* <p>
* Every instance runs this handler. Which one handles a given command is decided by the connector's routing, so the
* instance id in the reply shows where the command went.
*/
@Component
class HelloCommandHandler {

private final String instanceId;

HelloCommandHandler(Registration registration) {
this.instanceId = registration.getInstanceId();
}

@CommandHandler
String handle(SayHello command) {
return "Hello " + command.name() + ", from " + instanceId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.axoniq.springcloudcommandrouting;

import org.axonframework.messaging.commandhandling.gateway.CommandGateway;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.CompletableFuture;

/**
* Sends a {@link SayHello} command for every request, and replies with whatever the handling instance answered.
*/
@RestController
class HelloController {

private final CommandGateway commandGateway;

HelloController(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}

@GetMapping("/hello/{name}")
CompletableFuture<String> hello(@PathVariable String name) {
return commandGateway.send(new SayHello(name), String.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.axoniq.springcloudcommandrouting;

import org.axonframework.messaging.commandhandling.annotation.Command;

/**
* Asks to greet someone.
* <p>
* The {@code routingKey} tells the Spring Cloud connector to route by {@code name}: every command for the same name is
* handled by the same instance, no matter which instance it was sent from.
*
* @param name the name of whom to greet, and the key the command is routed by
*/
@Command(routingKey = "name")
public record SayHello(String name) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.axoniq.springcloudcommandrouting;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Starts one instance of the application. Start several, each on its own port, to form a cluster.
*/
@SpringBootApplication
public class SpringCloudCommandRoutingApplication {

public static void main(String[] args) {
SpringApplication.run(SpringCloudCommandRoutingApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
spring.application.name=spring-cloud-command-routing
server.port=${PORT:8080}

# Every instance registers with Consul under the same service name, and so joins the same cluster.
# The instance id has to be unique per instance, and is what the replies name.
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.instance-id=${spring.application.name}-${server.port}
# This sample has no actuator, so there is no health endpoint for Consul to check.
spring.cloud.consul.discovery.register-health-check=false
Loading