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
103 changes: 103 additions & 0 deletions order-fulfillment-workflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Order Fulfillment Workflow

Sample project showing how to model an order fulfillment process using the
**Axon Workflow Engine**. The workflow is written as plain imperative Java —
the engine handles event sourcing, crash recovery, and audit trails.

```java
@Workflow(idProperty = "orderId", startOnEvent = "io.axoniq.demo.orderfulfillment.api.OrderPlaced")
public void execute(SimpleWorkflowContext ctx) {
// Register the wait FIRST, so the workflow is subscribed before any payment-confirmation
// event has a chance to land.
var paymentConfirmation = ctx.waitForEvent("awaitPayment", PaymentConfirmed.class,
associate(payloadProperty("orderId"), equalsTo(orderId)), Duration.ofMinutes(15));

var reserved = ctx.awaitExecute("reserveStock", payload, Boolean.class, inventory::reserveStock);
if (!reserved) {
paymentConfirmation.cancel("Stock unavailable");
ctx.fail(new RuntimeException("Stock unavailable"));
return;
}
ctx.awaitExecute("initiatePayment", payload, payment::initiatePayment,
Duration.ofSeconds(30),
baseName("InitiatingPaymentForCustomer").namespace("io.axoniq.demo.orderfulfillment.api"));

paymentConfirmation.await();

// The Completed event of `shipOrder` (`ShipOrderCompleted`, in the api namespace) is what the
// projection subscribes to — no eventGateway.publish anywhere inside the workflow.
var shipResult = ctx.awaitExecute("shipOrder", payload, shipping::shipOrder,
Duration.ofSeconds(30), namespace("io.axoniq.demo.orderfulfillment.api"));
ctx.awaitExecute("notifyCustomer", Boolean.class, () -> { notifications.sendConfirmation(email); return true; });
}
```

## Prerequisites

- Java 21+
- Maven 3.9+
- Docker (for Axon Server)
- The Axon Workflow Engine (`io.axoniq.framework.workflow:*:1.0.0-SNAPSHOT`) installed in the local Maven repository

If the workflow engine isn't published yet, build it locally first:

```bash
git clone git@github.com:AxonIQ/extension-workflow.git
cd extension-workflow
mvn clean install -DskipTests
```

## Running the application

```bash
docker compose up -d
mvn spring-boot:run
```

The application starts on port **9090**.

### REST endpoints

| Method | Path | Description |
|--------|-------------------------------|---------------------------------------------|
| POST | `/orders?customerId=&email=&amount=` | Place a new order. Returns the order id. |
| POST | `/orders/{orderId}/payment` | Confirm payment and resume the workflow. |
| GET | `/orders/{orderId}` | Read the order's projected status. |

### Quick demo

```bash
# 1. place an order
ORDER=$(curl -s -X POST 'http://localhost:9090/orders?customerId=alice&email=alice@example.com&amount=99.95')

# 2. confirm payment
curl -X POST "http://localhost:9090/orders/$ORDER/payment"

# 3. observe the projected status
curl "http://localhost:9090/orders/$ORDER"
```

## Integration test

```bash
mvn verify
```

`OrderFulfillmentIT` boots the application against a real Axon Server (started
via Testcontainers), places an order, confirms payment, and asserts that the
projection reaches the `SHIPPED` state.

## What this sample demonstrates

* `@Workflow` with `idProperty` and `startOnEvent`
* `awaitExecute` with payload and typed return value
* `awaitExecute` with timeout and event-name customization (`baseName(...)`)
* `awaitEvent` correlated to the workflow instance via `associate(payloadProperty(...), equalsTo(...))`
* `ctx.fail(...)` to terminate a workflow with an error
* A standard `@EventHandler` projection consuming events emitted by the workflow's actions

## Cleanup

```bash
docker compose down -v
```
20 changes: 20 additions & 0 deletions order-fulfillment-workflow/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
axon-server:
image: docker.axoniq.io/axoniq/axonserver:2026.0.0
container_name: order-fulfillment-axon-server
ports:
- "8024:8024"
- "8124:8124"
environment:
axoniq.axonserver.standalone-dcb: true
axoniq_axonserver_hostname: axon-server
axoniq_axonserver_devmode_enabled: true
volumes:
- data:/axonserver/data
- events:/axonserver/events

volumes:
data:
driver: local
events:
driver: local
187 changes: 187 additions & 0 deletions order-fulfillment-workflow/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?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>

<groupId>io.axoniq.demo</groupId>
<artifactId>order-fulfillment-workflow</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Order Fulfillment Workflow</name>
<description>Sample showing how to model an Order Fulfillment process with the Axon Workflow Engine.</description>

<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<axon.version>5.1.0</axon.version>
<axoniq-framework.version>5.1.0</axoniq-framework.version>
<axon-workflow.version>1.0.0-SNAPSHOT</axon-workflow.version>
<spring-boot.version>4.0.6</spring-boot.version>
<testcontainers.version>2.0.5</testcontainers.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-framework-bom</artifactId>
<version>${axon.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.axoniq.framework</groupId>
<artifactId>axoniq-framework-bom</artifactId>
<version>${axoniq-framework.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Axon Workflow Engine: auto-configures @Workflow beans through Spring Boot. -->
<dependency>
<groupId>io.axoniq.framework.workflow</groupId>
<artifactId>axon-workflow-spring-boot</artifactId>
<version>${axon-workflow.version}</version>
</dependency>

<!-- Axon Framework Spring Boot integration -->
<dependency>
<groupId>org.axonframework.extensions.spring</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.axoniq.framework</groupId>
<artifactId>axon-server-connector</artifactId>
</dependency>

<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- Test -->
<dependency>
<groupId>io.axoniq.framework.workflow</groupId>
<artifactId>axon-workflow-test</artifactId>
<version>${axon-workflow.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-resttestclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-restclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.axoniq.framework</groupId>
<artifactId>axoniq-testcontainer</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.4</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>central-portal-snapshots</id>
<name>Central Portal Snapshots</name>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.axoniq.demo.orderfulfillment;

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

@SpringBootApplication
public class OrderFulfillmentApplication {

public static void main(String[] args) {
SpringApplication.run(OrderFulfillmentApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.axoniq.demo.orderfulfillment.api;

/**
* Emitted automatically by the workflow engine when the {@code initiatePayment} step starts.
* Used as a synchronisation point: by the time this event is in the store, the workflow has
* already registered its {@code awaitPayment} wait (the wait is registered at the very top
* of the workflow, before any {@code awaitExecute} step runs).
*/
public record InitiatingPaymentForCustomerStarted(String orderId) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.axoniq.demo.orderfulfillment.api;

import org.axonframework.messaging.eventhandling.annotation.Event;

@Event
public record OrderDelivered(
String orderId,
String trackingNumber
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.axoniq.demo.orderfulfillment.api;

import org.axonframework.messaging.eventhandling.annotation.Event;

@Event
public record OrderFailed(
String orderId,
String reason
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.axoniq.demo.orderfulfillment.api;

import org.axonframework.messaging.eventhandling.annotation.Event;

@Event
public record OrderPlaced(
String orderId,
String customerId,
String email,
double amount,
String originCity,
double originLat,
double originLng,
String destinationCity,
double destinationLat,
double destinationLng,
String scenario
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.axoniq.demo.orderfulfillment.api;

import org.axonframework.messaging.eventhandling.annotation.Event;

@Event
public record PaymentConfirmed(
String orderId,
String transactionId
) {
}
Loading
Loading