AsyncAPI Shopping Cart
Event-driven shopping cart application demonstrating AsyncAPI with Avro schemas and ZenWave SDK code generation.
This is a complete example of an event-driven application built with AsyncAPI and Avro schemas, showcasing how ZenWave SDK generates production-ready Kafka producers and consumers. You can find the Complete Source Code at GitHub.
What You'll Learn
- Define multi-message AsyncAPI contracts using Avro schemas
- Generate complete Kafka producers and consumers using ZenWaveSDK
- Implement event-driven architecture with Spring Boot and Kafka
- Prevent API drift by sourcing versioned AsyncAPI definitions
- Test event-driven applications with in-memory implementations
We assume you have already read the Getting Started section and installed ZenWave SDK CLI and are somewhat familiar with the concepts of Event-Driven Architecture and AsyncAPI.
What we will be building: An Event-Driven Shopping Cart
We will be building a multi-module event-driven application that demonstrates how to use AsyncAPI with Avro schemas to implement a shopping cart system with Kafka messaging.
The application is composed of three modules which in real-world scenarios would be in different repositories with independent lifecycles:
apis- Contains the AsyncAPI+Avro definition files which can be packaged and distributed as a Maven JAR artifactshopping-cart- The provider application that exposes a REST API and produces events to a Kafka topicclient- A consumer application that consumes events from the Kafka topic and logs them
This example showcases how you can send multiple Avro subjects to the same Kafka topic when you need strict ordering of events of different types. The sequence "ItemAdded, ItemUpdated, ItemRemoved" is fundamentally different from "ItemRemoved, ItemUpdated, ItemAdded", and this pattern ensures proper event ordering.
AsyncAPI Definition with Avro Schemas
The AsyncAPI definition contains one channel ShoppingCartChannel with 5 messages in Avro format, and 5 different operations, each sending one particular message to the channel:
AsyncAPI Definition for Shopping Cart Events
asyncapi: 3.0.0info:title: "AsyncAPI Shopping Cart Example"version: 0.0.1tags:- name: "Default"- name: "ShoppingCart"defaultContentType: application/jsonchannels:ShoppingCartChannel:address: "shopping-cart"messages:ShoppingCartItemUpdatedMessage:$ref: '#/components/messages/ShoppingCartItemUpdatedMessage'ShoppingCartCheckedOutMessage:$ref: '#/components/messages/ShoppingCartCheckedOutMessage'ShoppingCartItemAddedMessage:$ref: '#/components/messages/ShoppingCartItemAddedMessage'ShoppingCartCreatedMessage:$ref: '#/components/messages/ShoppingCartCreatedMessage'ShoppingCartItemRemovedMessage:$ref: '#/components/messages/ShoppingCartItemRemovedMessage'operations:onShoppingCartCreated:action: sendtags:- name: ShoppingCartchannel:$ref: '#/channels/ShoppingCartChannel'messages:- $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartCreatedMessage'onShoppingCartItemAdded:action: sendtags:- name: ShoppingCartchannel:$ref: '#/channels/ShoppingCartChannel'messages:- $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartItemAddedMessage'onShoppingCartItemRemoved:action: sendtags:- name: ShoppingCartchannel:$ref: '#/channels/ShoppingCartChannel'messages:- $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartItemRemovedMessage'onShoppingCartItemUpdated:action: sendtags:- name: ShoppingCartchannel:$ref: '#/channels/ShoppingCartChannel'messages:- $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartItemUpdatedMessage'onShoppingCartCheckedOut:action: sendtags:- name: ShoppingCartchannel:$ref: '#/channels/ShoppingCartChannel'messages:- $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartCheckedOutMessage'components:messages:ShoppingCartCreatedMessage:name: ShoppingCartCreatedMessagetitle: ""summary: ""traits:- $ref: '#/components/messageTraits/CommonHeaders'payload:schemaFormat: application/vnd.apache.avro+json;version=1.9.0schema:$ref: "./avro/ShoppingCartCreated.avsc"ShoppingCartItemAddedMessage:name: ShoppingCartItemAddedMessagetitle: ""summary: ""traits:- $ref: '#/components/messageTraits/CommonHeaders'payload:schemaFormat: application/vnd.apache.avro+json;version=1.9.0schema:$ref: "./avro/ShoppingCartItemAdded.avsc"ShoppingCartItemRemovedMessage:name: ShoppingCartItemRemovedMessagetitle: ""summary: ""traits:- $ref: '#/components/messageTraits/CommonHeaders'payload:schemaFormat: application/vnd.apache.avro+json;version=1.9.0schema:$ref: "./avro/ShoppingCartItemRemoved.avsc"ShoppingCartItemUpdatedMessage:name: ShoppingCartItemUpdatedMessagetitle: ""summary: ""traits:- $ref: '#/components/messageTraits/CommonHeaders'payload:schemaFormat: application/vnd.apache.avro+json;version=1.9.0schema:$ref: "./avro/ShoppingCartItemUpdated.avsc"ShoppingCartCheckedOutMessage:name: ShoppingCartCheckedOutMessagetitle: ""summary: ""traits:- $ref: '#/components/messageTraits/CommonHeaders'payload:schemaFormat: application/vnd.apache.avro+json;version=1.9.0schema:$ref: "./avro/ShoppingCartCheckedOut.avsc"messageTraits:CommonHeaders:headers:type: objectproperties:kafka_messageKey:type: "integer"format: "int64"description: This header value will be populated automatically at runtimex-runtime-expression: $message.payload#/customerId# CloudEvents Attributesce-id:type: stringdescription: Unique identifier for the eventx-runtime-expression: $message.payload#{#this.customerId.toString()}ce-source:type: stringdescription: URI identifying the context where event happenedx-runtime-expression: $message.payload#{"ShoppingCart"}ce-specversion:type: stringdescription: CloudEvents specification versionx-runtime-expression: $message.payload#{"1.0"}ce-type:type: stringdescription: Event typex-runtime-expression: $message.payload#{#this.getClass().getSimpleName()}ce-time:type: stringdescription: Timestamp of when the event happenedx-runtime-expression: $message.payload#{T(java.time.Instant).now().toString()}
The AsyncAPI definition references Avro schema files (*.avsc) that define the structure of each event message:
ShoppingCartCreated.avsc - Event Schema
{"type" : "record","name" : "ShoppingCartCreated","namespace" : "io.example.asyncapi.shoppingcart.events.avro","fields" : [ {"name" : "customerId","type" : "long"} ]}
ShoppingCartItemAdded.avsc - Event Schema
{"type" : "record","name" : "ShoppingCartItemAdded","namespace" : "io.example.asyncapi.shoppingcart.events.avro","fields" : [ {"name" : "customerId","type" : "long"}, {"name" : "shoppingCart","type" : "ShoppingCart"}, {"name" : "item","type" : "Item"} ]}
Item.avsc - Supporting Schema
{"type" : "record","name" : "Item","namespace" : "io.example.asyncapi.shoppingcart.events.avro","fields" : [ {"name" : "name","type" : "string"}, {"name" : "quantity","type" : "int"} ]}
ShoppingCart.avsc - Supporting Schema
{"type" : "record","name" : "ShoppingCart","namespace" : "io.example.asyncapi.shoppingcart.events.avro","fields" : [ {"name" : "id","type" : "long"}, {"name" : "customerId","type" : "long"}, {"name" : "items","type" : {"type" : "array","items" : "Item","java-class" : "java.util.List"}} ]}
Building with ZenWave SDK and AsyncAPI
The shopping cart application demonstrates how ZenWave SDK generates a complete event-driven architecture from AsyncAPI definitions. All generated code is recreated on each build, ensuring your implementation stays in sync with your API definitions.
Code Generation from AsyncAPI
ZenWave SDK's AsyncAPI Generator creates a complete SDK for producing events to Kafka:
- Avro DTOs for all messages defined in the AsyncAPI definition
- Producer Interface (
ShoppingCartEventsProducer) with one method per operation - Spring Implementation (
DefaultShoppingCartEventsProducer) as a Spring@Component - In-Memory Implementation for unit testing purposes
- Test Context (
EventsProducerInMemoryContext) for easier testing
All this code is generated as part of your Maven build and placed in target/generated-sources/zenwave-sdk. Because it's not part of the project source code, it's not versioned in Git. To modify the generated code, you change the AsyncAPI+Avro definition files first and then rebuild the project.
Maven Configuration for Code Generation
The Maven configuration uses ZenWave SDK Maven Plugin to generate code from AsyncAPI definitions. The plugin can source AsyncAPI+Avro definitions from local files, classpath resources, or remote HTTP resources.
Maven Plugin Configuration in pom.xml
<plugin><groupId>io.zenwave360.sdk</groupId><artifactId>zenwave-sdk-maven-plugin</artifactId><version>${zenwave.version}</version><configuration><inputSpec>${asyncapi.inputSpec}</inputSpec><skip>false</skip><addCompileSourceRoot>true</addCompileSourceRoot><addTestCompileSourceRoot>true</addTestCompileSourceRoot><!-- <authentication> --><!-- <authentication><key>API_KEY</key><value>XXX</value></authentication> --><!-- </authentication> --></configuration><executions><execution><id>generate-asyncapi</id><phase>generate-sources</phase><goals><goal>generate</goal></goals><configuration><generatorName>AsyncAPIGenerator</generatorName><configOptions><role>provider</role><templates>${zenwave.asyncapiGenerator.templates}</templates><!-- <modelPackage>${asyncApiModelPackage}</modelPackage> required for json-schema, here it'll use the avro package --><producerApiPackage>${asyncApiProducerApiPackage}</producerApiPackage><consumerApiPackage>${asyncApiConsumerApiPackage}</consumerApiPackage><avroCompilerProperties.imports>${asyncapi.avro.imports}</avroCompilerProperties.imports></configOptions></configuration></execution></executions><dependencies><dependency><groupId>io.zenwave360.sdk.plugins</groupId><artifactId>asyncapi-generator</artifactId><version>${zenwave.version}</version></dependency><dependency><groupId>org.apache.avro</groupId><artifactId>avro-compiler</artifactId><version>${avro.version}</version><exclusions><exclusion><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId></exclusion><exclusion><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></exclusion></exclusions></dependency><dependency><groupId>io.example.asyncapi.shoppingcart</groupId><artifactId>shopping-cart-apis</artifactId><version>1.0.0</version></dependency></dependencies></plugin>
The key configuration properties are:
Maven Properties for AsyncAPI Code Generation in pom.xml
<properties><basePackage>io.example.asyncapi.shoppingcart</basePackage><openApiApiPackage>${basePackage}.web</openApiApiPackage><openApiModelPackage>${basePackage}.web.model</openApiModelPackage><asyncapiPrefix>classpath:io/example/asyncapi/shoppingcart/apis</asyncapiPrefix><asyncapi.inputSpec>${asyncapiPrefix}/asyncapi.yml</asyncapi.inputSpec><asyncapi.avro.imports>${asyncapiPrefix}/avro/</asyncapi.avro.imports><zenwave.asyncapiGenerator.templates>SpringKafka</zenwave.asyncapiGenerator.templates><asyncApiProducerApiPackage>${basePackage}.events</asyncApiProducerApiPackage><asyncApiConsumerApiPackage>${basePackage}.commands</asyncApiConsumerApiPackage><spotless-maven-plugin.version>2.44.3</spotless-maven-plugin.version><palantir-java-format.version>2.55.0</palantir-java-format.version></properties>
Or for remote HTTP resources:
Maven Properties for AsyncAPI Code Generation from HTTP resources in pom.xml
<profile><id>asyncapi-http-files</id><properties><asyncapiPrefix>https://raw.githubusercontent.com/ZenWave360/zenwave-playground/refs/heads/main/examples/asyncapi-shopping-cart/apis</asyncapiPrefix><asyncapi.inputSpec>${asyncapiPrefix}/asyncapi.yml</asyncapi.inputSpec><asyncapi.avro.imports>${asyncapiPrefix}/avro/Item.avsc,${asyncapiPrefix}/avro/ShoppingCart.avsc,</asyncapi.avro.imports></properties></profile>
Note: When using remote HTTP resources,
asyncapi.avro.importsrequires pointing to each avsc file individually because it is not possible to scan folders or packages from remote HTTP resources.
Note: You do not need to worry about ordering
asyncapi.avro.importseven if you are using Avro prior to 1.12.0 because ZenWaveSDK will sort them for you.
Using Generated Code in Your Application
The generated producer interface integrates seamlessly into your Spring Boot application. Here's how the generated code is used in the service implementation:
@Service@lombok.AllArgsConstructorpublic class ShoppingCartServiceImpl implements ShoppingCartService {// [...]private final ShoppingCartEventsProducer eventsProducer; // <-- this is generated codepublic ShoppingCart addItem(Long customerId, Item input) {log.debug("Request addItem: {} {}", customerId, input);var shoppingCart = loadShoppingCart(customerId);shoppingCart.getItems().add(input);shoppingCartRepository.save(shoppingCart);// emit eventsvar shoppingCartItemAdded = eventsMapper.asShoppingCartItemAdded(shoppingCart, input);eventsProducer.onShoppingCartItemAdded(shoppingCartItemAdded);return shoppingCart;}}
The service implementation receives the generated ShoppingCartEventsProducer via dependency injection and uses it to publish events after performing business operations. The producer interface provides type-safe methods for each operation defined in the AsyncAPI specification.
Unit Testing with In-Memory Event Producer
ZenWave SDK generates an in-memory implementation of the events producer for unit testing purposes, allowing you to test your business logic without requiring a running Kafka instance:
class ShoppingCartServiceTest {ServicesInMemoryConfig context = new ServicesInMemoryConfig();ShoppingCartServiceImpl shoppingCartService = context.shoppingCartService();@BeforeEach void setUp() {context.reloadTestData();}@Test void createShoppingCartTest() {Long customerId = 0L; // non existing shopping cartvar shoppingCart = shoppingCartService.loadShoppingCart(customerId);Assertions.assertNotNull(shoppingCart);var capturedMessages = context.getEventsProducerInMemoryContext().shoppingCartEventsProducer().getOnShoppingCartCreatedCapturedMessages();Assertions.assertEquals(1, capturedMessages.size());}}
The in-memory producer captures all published events, allowing you to verify that your service publishes the correct events with the expected payloads. This approach provides fast, reliable unit tests without external dependencies.
Shopping Cart Client Application
The client application demonstrates how to consume events from Kafka using ZenWave SDK generated code. It uses the same AsyncAPI definition but with reversed roles.
Generating Consumer Code
The client uses similar Maven configuration to generate all required code to consume events from the Kafka topic. The key difference is using <role>client</role> in the ZenWave SDK AsyncAPI Generator configuration, which reverses how code is generated: generating a consumer where the API defines a producer and vice versa.
Client Maven Configuration in pom.xml
<plugin><groupId>io.zenwave360.sdk</groupId><artifactId>zenwave-sdk-maven-plugin</artifactId><version>${zenwave.version}</version><configuration><inputSpec>${asyncapi.inputSpec}</inputSpec><skip>false</skip><addCompileSourceRoot>true</addCompileSourceRoot><addTestCompileSourceRoot>true</addTestCompileSourceRoot></configuration><executions><execution><id>generate-asyncapi</id><phase>generate-sources</phase><goals><goal>generate</goal></goals><configuration><generatorName>AsyncAPIGenerator</generatorName><configOptions><role>client</role><templates>${zenwave.asyncapiGenerator.templates}</templates><!-- required for json, here it uses avro package <modelPackage>${asyncApiModelPackage}</modelPackage>--><producerApiPackage>${asyncApiProducerApiPackage}</producerApiPackage><consumerApiPackage>${asyncApiConsumerApiPackage}</consumerApiPackage><avroCompilerProperties.imports>${asyncapi.avro.imports}</avroCompilerProperties.imports><!-- <operationIds>onCustomerDeletedEvent</operationIds>--></configOptions></configuration></execution></executions><dependencies><dependency><groupId>io.zenwave360.sdk.plugins</groupId><artifactId>asyncapi-generator</artifactId><version>${zenwave.version}</version></dependency><dependency><groupId>org.apache.avro</groupId><artifactId>avro-compiler</artifactId><version>${avro.version}</version><exclusions><exclusion><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId></exclusion><exclusion><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></exclusion></exclusions></dependency><dependency><groupId>io.example.asyncapi.shoppingcart</groupId><artifactId>shopping-cart-apis</artifactId><version>1.0.0</version></dependency></dependencies></plugin>
Naming Convention for Operations
Because the generator uses the same operationIds to generate Java code, we recommend following this naming convention for consistency between provider and client:
- Use prefix
on*for operations that produce event messages (e.g.,onShoppingCartItemAdded) - Use prefix
do*for operations that consume command messages (e.g.,doCheckoutShoppingCart)
Implementing Event Consumers
After code is generated, you can focus on your business logic by implementing the generated consumer interface:
ShoppingCartChannelConsumerService.java - Event Consumer Implementation
@Componentpublic class ShoppingCartChannelConsumerService implements IShoppingCartChannelConsumerService {private static final Logger log = LoggerFactory.getLogger(ShoppingCartChannelConsumerService.class);@Overridepublic void onShoppingCartCreated(ShoppingCartCreated payload, ShoppingCartCreatedHeaders headers) {log.info("onShoppingCartCreated: {}", payload);}
The consumer service implements the generated interface, providing type-safe methods for handling each event type. Spring Cloud Streams handles all the Kafka consumer configuration, deserialization, and message routing automatically.
Running the Shopping Cart Application
Prerequisites
- JDK 25+
- Maven 3.8+
- Docker & Docker Compose - If you don't have Docker Compose installed, we recommend Rancher Desktop configured with
dockerdengine (notcontainerd), which includes bothdockeranddocker-composecommands. - Your favorite IDE
Quick Start
Follow these steps to run the complete application:
-
Package AsyncAPI+Avro definition files:
ZenWaveSDK AsyncAPI generator can source AsyncAPI+Avro definition files from local files, classpath resources, or remote HTTP resources. Build and install the
apismodule in your local Maven repository:mvn clean install -f apis/pom.xmlAlternatively, you can append
-Pasyncapi-http-filesto the commands below to source the AsyncAPI+Avro definition files from GitHub HTTP URLs. -
Start infrastructure services:
docker-compose -f shopping-cart/docker-compose.yml up -d -
Run the Shopping Cart Provider application:
mvn clean spring-boot:run -f shopping-cart/pom.xmlNote: Append
-Pasyncapi-http-filesto the previous command if you prefer to source the AsyncAPI+Avro definition files from remote HTTP resources. -
Access the API:
- Open Swagger UI in your browser
- Use Basic Authentication: username
admin, passwordpassword
-
Test the endpoints:
- Try creating a shopping cart and adding items
- Update item quantities
- Check out the shopping cart
Verifying Event Flow
Logging Kafka Events
You can verify that events are being published to Kafka by using the Kafka Avro console consumer:
docker-compose -f shopping-cart/docker-compose.yml \exec -T schema-registry bash -c "kafka-avro-console-consumer --bootstrap-server kafka:19093 \--topic shopping-cart \--from-beginning \--property schema.registry.url=http://schema-registry:8081"
You should see the events produced by the shopping cart provider application:
{"customerId":7}{"customerId":7,"shoppingCart":{"id":2,"customerId":7,"items":[{"name":"Item Name","quantity":10}]},"item":{"name":"Item Name","quantity":10}}{"customerId":7,"shoppingCart":{"id":2,"customerId":7,"items":[{"name":"Item Name","quantity":10}]},"item":{"name":"Item Name","quantity":10},"previousItem":{"name":"Item Name","quantity":10}}
Running the Client Consumer
Start the shopping cart client application to consume and process events:
mvn clean spring-boot:run -f client/pom.xml
You should see the client application logging the events as they are consumed:
2025-11-22T11:58:29.112+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartCreated: {"customerId": 1}2025-11-22T11:58:29.116+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartItemAdded: {"customerId": 1, "shoppingCart": {"id": 1, "customerId": 1, "items": [{"name": "string", "quantity": 0}]}, "item": {"name": "string", "quantity": 0}}2025-11-22T11:58:29.124+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartCreated: {"customerId": 7}2025-11-22T11:58:29.126+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartItemAdded: {"customerId": 7, "shoppingCart": {"id": 2, "customerId": 7, "items": [{"name": "Item Name", "quantity": 10}]}, "item": {"name": "Item Name", "quantity": 10}}2025-11-22T11:58:29.128+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartItemUpdated: {"customerId": 7, "shoppingCart": {"id": 2, "customerId": 7, "items": [{"name": "Item Name", "quantity": 10}]}, "item": {"name": "Item Name", "quantity": 10}, "previousItem": {"name": "Item Name", "quantity": 10}}
What's Running
- Kafka (port 9092) - Event streaming platform
- Schema Registry (port 8081) - Avro schema management
- Shopping Cart Provider (port 8080) - REST API and event producer
- Shopping Cart Client - Event consumer and processor
Happy Coding! ๐