Customer Address JPA Service
Canonical ZenWave example of a DDD Aggregate with JPA persistence and Externalized Domain Events.
This is a canonical example of a ZenWave designed and generated project, around a DDD Aggregate persisted with JPA and externalized Domain Events published to a Kafka topic. You can find the Complete Source Code at GitHub.
What You'll Learn
- How to model a DDD aggregate using ZDL
- Generate REST APIs from domain models
- Implement event-driven architecture with AsyncAPI
- Create a complete Spring Boot microservice
We assume you have already read the Getting Started section and installed ZenWave SDK CLI and IntelliJ Plugin and are somewhat familiar with the concepts of DDD and Event-Driven Architecture.
What we will be building: A Customer Master Data Service
We will be building a Java/SpringBoot microservice exposing CRUD operations for a Customer
aggregate as a REST API and publishing Domain Events to Kafka.
The Customer
entity is the root of the aggregate, holding relationships to other entities:
- an array of
Address
entities stored in a JSON column in the database and - a
@OneToMany
collection ofPaymentMethod
entities.
NOTE: In this example, the Customer
aggregate represents a cluster of related entities that are persisted and managed as a single unit. However, the business logic resides in the CustomerService
class rather than within the entities themselves.
This pattern is sometimes called an "Anemic Domain Model." For this particular example, the domain complexity is relatively simple and doesn't justify implementing a Rich Domain Model with business logic embedded in the aggregate entities.
REST API defined with OpenAPI
With these REST Endpoints:
Externalized Domain Events with AsyncAPI
And these Domain Events published to a Kafka Topic:
Building with ZenWave Domain Model and SDK
When modeling a microservice with ZenWave SDK we usually do it using two main files:
- A
zenwave-model.zdl
file containing the domain model and service definitions, we use this file to iterate and refine the domain model. - A
zenwave-scripts.zw
file containing the plugin configurations and executions, you can run each plugin individually from ZenWave Model Editor for IntelliJ to generate different aspects of the application.
The full application we are building in this example was defined in the following ZDL model:
`zenwave-model.zdl` for Customer Master Data Service
/*** Sample ZenWave Model Definition.* Use zenwave-scripts.zdl to generate your code from this model definition.*/config {title "ZenWave Customer JPA Example"basePackage "io.zenwave360.example"persistence jpadatabaseType postgresqllayout CleanHexagonalProjectLayout// these should match the values of openapi-generator-maven-plugin// used by the OpenAPIControllersPlugin and SpringWebTestClientPluginopenApiApiPackage "{{basePackage}}.adapters.web"openApiModelPackage "{{basePackage}}.adapters.web.model"openApiModelNameSuffix DTO}/*** Customer entity*/@aggregate@auditing // adds auditing fields to the entityentity Customer {name String required maxlength(254) /** Customer name */email String required maxlength(254)/** Customer Addresses can be stored in a JSON column in the database. */@json addresses Address[] minlength(1) maxlength(5) {street String required maxlength(254)city String required maxlength(254)}}@auditingentity PaymentMethod {type PaymentMethodType requiredcardNumber String required}enum PaymentMethodType { VISA(1), MASTERCARD(2) }relationship OneToMany {Customer{paymentMethods required maxlength(3)} to PaymentMethod{customer required}}// you can create 'inputs' as dtos for your service methods, or use entities directlyinput CustomerSearchCriteria {name Stringemail Stringcity Stringstate String}@rest("/customers")service CustomerService for (Customer) {@postcreateCustomer(Customer) Customer withEvents CustomerEvent@get("/{id}")getCustomer(id) Customer?@put("/{id}")updateCustomer(id, Customer) Customer? withEvents CustomerEvent@delete("/{id}")deleteCustomer(id) withEvents CustomerDeletedEvent@post("/search")@paginatedsearchCustomers(CustomerSearchCriteria) Customer[]}@copy(Customer)@asyncapi({ channel: "CustomersChannel", topic: "customers" })event CustomerEvent {id Long requiredversion Integer// all fields from Customer are copied here, but not relationshipspaymentMethods PaymentMethod[]}@asyncapi({ channel: "CustomersChannel", topic: "customers" })event CustomerDeletedEvent {id Long required}
`zenwave-scripts.zw` for Customer Master Data Service
config {zdlFile "zenwave-model.zdl"plugins {ZDLToOpenAPIPlugin {idType integeridTypeFormat int64targetFile "src/main/resources/public/apis/openapi.yml"}ZDLToAsyncAPIPlugin {asyncapiVersion v3schemaFormat avroavroPackage "io.zenwave360.example.core.outbound.events.dtos"idType longtargetFile "src/main/resources/public/apis/asyncapi.yml"includeKafkaCommonHeaders true}BackendApplicationDefaultPlugin {useLombok trueincludeEmitEventsImplementation true// --force // overwite all files}OpenAPIControllersPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}SpringWebTestClientPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}SpringWebTestClientPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"groupBy businessFlowbusinessFlowTestName CreateUpdateDeleteCustomerIntegrationTestoperationIds createCustomer,updateCustomer,deleteCustomer,getCustomer}OpenAPIKaratePlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}OpenAPIKaratePlugin {openapiFile "src/main/resources/public/apis/openapi.yml"groupBy businessFlowbusinessFlowTestName CreateUpdateDeleteCustomerKarateTestoperationIds createCustomer,updateCustomer,deleteCustomer,getCustomer}}}
NOTE: You can name this files as you wish, just mind the file extension
.zdl
for the domain model and.zw
for the scripts and in.zw
pointing to the properzdlFile
file containing the domain model.
So let's dive into the details of how this application was built using ZenWave SDK.
Model Configuration
Let's start with the configuration section of the ZDL model:
ZenWave Model Configuration for Customer Master Data Service
/*** Sample ZenWave Model Definition.* Use zenwave-scripts.zdl to generate your code from this model definition.*/config {title "ZenWave Customer JPA Example"basePackage "io.zenwave360.example"persistence jpadatabaseType postgresqllayout CleanHexagonalProjectLayout// these should match the values of openapi-generator-maven-plugin// used by the OpenAPIControllersPlugin and SpringWebTestClientPluginopenApiApiPackage "{{basePackage}}.adapters.web"openApiModelPackage "{{basePackage}}.adapters.web.model"openApiModelNameSuffix DTO}
Basic settings:
title
andbasePackage
are self-explanatory and used throughout ZenWave SDK plugins for code and documentation generation.persistence
tells theBackendApplicationDefaultPlugin
which data store to target (jpa
ormongodb
), generating the appropriate Spring Data interfaces and controlling entity ID data types.databaseType
works withjpa
persistence to generate proper Hibernate configuration for ID generation strategies.layout
determines the architectural structure generated byBackendApplicationDefaultPlugin
. TheCleanHexagonalProjectLayout
creates a clean/hexagonal architecture with distinct layers: core domain, web adapters, and infrastructure. Note that JPA repository implementations are auto-generated by Spring Data, so no manual repository code is needed.
Additional configurations customizing the project layout:
openApiApiPackage
,openApiModelPackage
, andopenApiModelNameSuffix
are used by theOpenAPIControllersPlugin
and should match the values configured in theopenapi-generator-maven-plugin
in thepom.xml
file.
OpenAPI Generator Maven Plugin Configuration in pom.xml
<plugin><groupId>org.openapitools</groupId><artifactId>openapi-generator-maven-plugin</artifactId><version>7.10.0</version><executions><execution><goals><goal>generate</goal></goals><phase>generate-sources</phase><configuration><inputSpec>${project.basedir}/src/main/resources/public/apis/openapi.yml</inputSpec><skipIfSpecIsUnchanged>true</skipIfSpecIsUnchanged><generatorName>spring</generatorName><apiPackage>${openApiApiPackage}</apiPackage><modelPackage>${openApiModelPackage}</modelPackage><modelNameSuffix>DTO</modelNameSuffix><addCompileSourceRoot>true</addCompileSourceRoot><generateSupportingFiles>false</generateSupportingFiles><typeMappings><typeMapping>Double=java.math.BigDecimal</typeMapping></typeMappings><configOptions><useSpringBoot3>true</useSpringBoot3><documentationProvider>none</documentationProvider><openApiNullable>false</openApiNullable><useOptional>false</useOptional><useTags>true</useTags><interfaceOnly>true</interfaceOnly><skipDefaultInterface>true</skipDefaultInterface><delegatePattern>false</delegatePattern><sortParamsByRequiredFlag>false</sortParamsByRequiredFlag></configOptions></configuration></execution></executions></plugin>
Domain Modeling
Domain modeling starts with the entity
declarations. Entities body contains fields, nested entities. id
and version
fields are added automatically to entities. Entities decorated with @auditing
will also have createdDate
, createdBy
, lastModifiedDate
, and lastModifiedBy
fields added automatically.
Entities decorated with @aggregate
are considered the root of an aggregate and a Spring Data repository is generated for them and can be used on services
.
Nested entities are equivalente to entities decorated with @embedded
which are not persisted separately, they are part of the aggregate root entity:
- In
mongodb
they are stored as nested documents, and they don't have an id or version fields. - In
jpa
they are stored either as@Embeddable
or as a JSON column in the database. In this particular example, we are using a JSON column to store theaddresses
array in theCustomer
entity. (Arrays are not supported as@Embeddable
in JPA)
Relationships are modeled following JHipster JDL syntax and the corresponding fields are added to entities. In DDD, relationships between aggregates should typically be mapped only by their id. ZenWave SDK allows you to use @OneToXXX
relationships between aggregates, which are mapped by their id and include a read-only reference to the related entity, enabling a richer domain model in the ZDL while maintaining DDD principles. This pattern is not used in this example, but it's good to know this capability exists.
ZenWave Domain Model for Customer Master Data Service
/*** Customer entity*/@aggregate@auditing // adds auditing fields to the entityentity Customer {name String required maxlength(254) /** Customer name */email String required maxlength(254)/** Customer Addresses can be stored in a JSON column in the database. */@json addresses Address[] minlength(1) maxlength(5) {street String required maxlength(254)city String required maxlength(254)}}@auditingentity PaymentMethod {type PaymentMethodType requiredcardNumber String required}enum PaymentMethodType { VISA(1), MASTERCARD(2) }relationship OneToMany {Customer{paymentMethods required maxlength(3)} to PaymentMethod{customer required}}
Customer Entity Generated by ZenWave SDK
@lombok.Getter@lombok.Setter@Entity@Table(name = "customer")@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)@EntityListeners(AuditingEntityListener.class)public class Customer implements Serializable {@java.io.Serialprivate static final long serialVersionUID = 1L;@Id@GeneratedValue(strategy = GenerationType.SEQUENCE)private Long id;@Versionprivate Integer version;/** Customer name */@NotNull@Size(max = 254)@Column(name = "name", nullable = false, length = 254)private String name;@NotNull@Size(max = 254)@Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,4}")@Column(name = "email", nullable = false, length = 254)private String email;/** Customer Addresses can be stored in a JSON column in the database. */@Size(min = 1, max = 5)@org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.JSON)@Column(name = "addresses")private List<Address> addresses = new ArrayList<>();@NotNull@Size(max = 3)@OneToMany(mappedBy = "customer", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)private Set<PaymentMethod> paymentMethods = new HashSet<>();@CreatedBy@Column(name = "created_by", updatable = false)protected String createdBy;@CreatedDate@Column(name = "created_date", columnDefinition = "TIMESTAMP", updatable = false)protected LocalDateTime createdDate;@LastModifiedBy@Column(name = "last_modified_by")protected String lastModifiedBy;@LastModifiedDate@Column(name = "last_modified_date", columnDefinition = "TIMESTAMP")protected LocalDateTime lastModifiedDate;// manage relationshipspublic Customer addPaymentMethods(PaymentMethod paymentMethods) {this.paymentMethods.add(paymentMethods);paymentMethods.setCustomer(this);return this;}public Customer removePaymentMethods(PaymentMethod paymentMethods) {this.paymentMethods.remove(paymentMethods);paymentMethods.setCustomer(null);return this;}/** https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-* with-jpa-and-hibernate/*/@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (!(o instanceof Customer)) {return false;}Customer other = (Customer) o;return getId() != null && getId().equals(other.getId());}@Overridepublic int hashCode() {return getClass().hashCode();}}
Address Nested Entity Generated by ZenWave SDK
package io.zenwave360.example.core.domain;import jakarta.persistence.Column;import jakarta.validation.constraints.NotNull;import jakarta.validation.constraints.Size;import java.io.Serializable;/** */@lombok.Getter@lombok.Setter// @Embeddable // json embeddedpublic class Address implements Serializable {@java.io.Serialprivate static final long serialVersionUID = 1L;@NotNull@Size(max = 254)@Column(name = "street", nullable = false, length = 254)private String street;@NotNull@Size(max = 254)@Column(name = "city", nullable = false, length = 254)private String city;}
PaymentMethod Nested Entity Generated by ZenWave SDK
package io.zenwave360.example.core.domain;import jakarta.persistence.*;import jakarta.validation.constraints.NotNull;import org.hibernate.annotations.Cache;import org.hibernate.annotations.CacheConcurrencyStrategy;import org.springframework.data.annotation.CreatedBy;import org.springframework.data.annotation.CreatedDate;import org.springframework.data.annotation.LastModifiedBy;import org.springframework.data.annotation.LastModifiedDate;import org.springframework.data.jpa.domain.support.AuditingEntityListener;import java.io.Serializable;import java.time.LocalDateTime;/** */@lombok.Getter@lombok.Setter@Entity@Table(name = "payment_method")@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)@EntityListeners(AuditingEntityListener.class)public class PaymentMethod implements Serializable {@java.io.Serialprivate static final long serialVersionUID = 1L;@Id@GeneratedValue(strategy = GenerationType.SEQUENCE)@SequenceGenerator(name = "sequenceGenerator")private Long id;@Versionprivate Integer version;@NotNull@Column(name = "type", nullable = false)@Convert(converter = PaymentMethodType.PaymentMethodTypeConverter.class)private PaymentMethodType type;@NotNull@Column(name = "card_number", nullable = false)private String cardNumber;@NotNull@ManyToOne(fetch = FetchType.LAZY)@JoinColumn(name = "customer_id")private Customer customer;@CreatedBy@Column(name = "created_by", updatable = false)protected String createdBy;@CreatedDate@Column(name = "created_date", columnDefinition = "TIMESTAMP", updatable = false)protected LocalDateTime createdDate;@LastModifiedBy@Column(name = "last_modified_by")protected String lastModifiedBy;@LastModifiedDate@Column(name = "last_modified_date", columnDefinition = "TIMESTAMP")protected LocalDateTime lastModifiedDate;/** https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-* with-jpa-and-hibernate/*/@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (!(o instanceof PaymentMethod)) {return false;}PaymentMethod other = (PaymentMethod) o;return getId() != null && getId().equals(other.getId());}@Overridepublic int hashCode() {return getClass().hashCode();}}
Enums are also generated as Java enums, with an autogenerated JPA AttributeConverter
which persists the enum the integer configured as enumValue
in the ZDL model.
PaymentMethodType Enum Generated by ZenWave SDK
package io.zenwave360.example.core.domain;import jakarta.persistence.AttributeConverter;import jakarta.persistence.Converter;import java.util.Arrays;/** Enum for PaymentMethodType. */public enum PaymentMethodType {VISA(1), MASTERCARD(2),;private final Integer value;private PaymentMethodType(Integer value) {this.value = value;}public Integer getValue() {return value;}public static PaymentMethodType fromValue(Integer value) {return Arrays.stream(PaymentMethodType.values()).filter(e -> e.value.equals(value)).findFirst().orElse(null);}@Converterstatic class PaymentMethodTypeConverter implements AttributeConverter<PaymentMethodType, Integer> {@Overridepublic Integer convertToDatabaseColumn(PaymentMethodType attribute) {if (attribute == null) {return null;}return attribute.value;}@Overridepublic PaymentMethodType convertToEntityAttribute(Integer dbData) {return PaymentMethodType.fromValue(dbData);}}}
Aggregates will also get generated a Spring Data Repository interface and an InMemory implementation for testing purposes.
CustomerRepository.java Generated by ZenWave SDK
package io.zenwave360.example.core.outbound.jpa;import io.zenwave360.example.core.domain.Customer;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;/** Spring Data JPA repository for the Customer entity. */@SuppressWarnings("unused")@Repositorypublic interface CustomerRepository extends JpaRepository<Customer, Long> {}
InMemoryCustomerRepository.java Generated by ZenWave SDK
package io.zenwave360.example.infrastructure.jpa.inmemory;import io.zenwave360.example.core.domain.Customer;import io.zenwave360.example.core.outbound.jpa.CustomerRepository;import static org.apache.commons.lang3.ObjectUtils.firstNonNull;public class CustomerRepositoryInMemory extends InMemoryJpaRepository<Customer> implements CustomerRepository {private long nextId = 0;private final PrimaryKeyGenerator<Long> primaryKeyGenerator = () -> nextId++;public Customer save(Customer entity) {entity = super.save(entity);entity.getPaymentMethods().forEach(paymentMethod -> {paymentMethod.setId(firstNonNull(paymentMethod.getId(), primaryKeyGenerator.next()));});return entity;}}
Services
Services are the entry point to the core domain, and are generated as Spring @Service
classes.
Customer Service for (Customer)
@rest("/customers")service CustomerService for (Customer) {@postcreateCustomer(Customer) Customer withEvents CustomerEvent@get("/{id}")getCustomer(id) Customer?@put("/{id}")updateCustomer(id, Customer) Customer? withEvents CustomerEvent@delete("/{id}")deleteCustomer(id) withEvents CustomerDeletedEvent@post("/search")@paginatedsearchCustomers(CustomerSearchCriteria) Customer[]}
This will generate a CustomerService
, a CustomerServiceImpl
referencing CustomerRepository
(because it's this service@aggregate
entity) and CustomerServiceTest
that uses an in-memory implementation of the repository for testing.
CustomerService.java Generated by ZenWave SDK
package io.zenwave360.example.core.inbound;import io.zenwave360.example.core.domain.Customer;import io.zenwave360.example.core.inbound.dtos.CustomerSearchCriteria;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import java.util.Optional;/** Inbound Service Port for managing [Customer]. */public interface CustomerService {/** With Events: [CustomerEvent]. */public Customer createCustomer(Customer input);/** */public Optional<Customer> getCustomer(Long id);/** With Events: [CustomerEvent]. */public Optional<Customer> updateCustomer(Long id, Customer input);/** With Events: [CustomerEvent]. */public void deleteCustomer(Long id);/** */public Page<Customer> searchCustomers(CustomerSearchCriteria input, Pageable pageable);}
CustomerServiceImpl.java Generated by ZenWave SDK
package io.zenwave360.example.core.implementation;import io.zenwave360.example.core.domain.Customer;import io.zenwave360.example.core.implementation.mappers.CustomerServiceMapper;import io.zenwave360.example.core.implementation.mappers.EventsMapper;import io.zenwave360.example.core.inbound.CustomerService;import io.zenwave360.example.core.inbound.dtos.CustomerSearchCriteria;import io.zenwave360.example.core.outbound.events.CustomerEventsProducer;import io.zenwave360.example.core.outbound.jpa.CustomerRepository;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.ApplicationEventPublisher;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.Optional;/** Service Implementation for managing [Customer]. */@Service@Transactional(readOnly = true)@lombok.AllArgsConstructorpublic class CustomerServiceImpl implements CustomerService {private final Logger log = LoggerFactory.getLogger(getClass());private final CustomerServiceMapper customerServiceMapper = CustomerServiceMapper.INSTANCE;private final CustomerRepository customerRepository;private final EventsMapper eventsMapper = EventsMapper.INSTANCE;private final CustomerEventsProducer eventsProducer;private final ApplicationEventPublisher applicationEventPublisher;@Transactionalpublic Customer createCustomer(Customer input) {log.debug("[CRUD] Request to save Customer: {}", input);var customer = customerServiceMapper.update(new Customer(), input);customer = customerRepository.save(customer);// TODO: may need to reload the entity to fetch relationships 'mapped by id'// emit eventsvar customerEvent = eventsMapper.asCustomerEvent(customer);eventsProducer.onCustomerEvent(customerEvent);return customer;}public Optional<Customer> getCustomer(Long id) {log.debug("[CRUD] Request to get Customer : {}", id);var customer = customerRepository.findById(id);return customer;}@Transactionalpublic Optional<Customer> updateCustomer(Long id, Customer input) {log.debug("Request updateCustomer: {} {}", id, input);var customer = customerRepository.findById(id).map(existingCustomer -> {return customerServiceMapper.update(existingCustomer, input);}).map(customerRepository::save);if (customer.isPresent()) {// emit eventsvar customerEvent = eventsMapper.asCustomerEvent(customer.get());eventsProducer.onCustomerEvent(customerEvent);}return customer;}@Transactionalpublic void deleteCustomer(Long id) {log.debug("[CRUD] Request to delete Customer : {}", id);customerRepository.deleteById(id);// emit eventsvar customerEvent = eventsMapper.asCustomerEvent(id);eventsProducer.onCustomerDeletedEvent(customerEvent);}public Page<Customer> searchCustomers(CustomerSearchCriteria input, Pageable pageable) {log.debug("Request searchCustomers: {} {}", input, pageable);var customers = customerRepository.findAll(pageable);return customers;}}
CustomerServiceTest.java Generated by ZenWave SDK
package io.zenwave360.example.core.implementation;import io.zenwave360.example.config.ServicesInMemoryConfig;import io.zenwave360.example.core.domain.Address;import io.zenwave360.example.core.domain.Customer;import io.zenwave360.example.core.inbound.dtos.CustomerSearchCriteria;import io.zenwave360.example.infrastructure.jpa.inmemory.CustomerRepositoryInMemory;import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.data.domain.PageRequest;import java.util.List;import static org.junit.jupiter.api.Assertions.*;/** Acceptance Test for CustomerService. */class CustomerServiceTest {private final Logger log = LoggerFactory.getLogger(getClass());ServicesInMemoryConfig context = new ServicesInMemoryConfig();CustomerServiceImpl customerService = context.customerService();CustomerRepositoryInMemory customerRepository = context.customerRepository();@BeforeEachvoid setUp() {context.reloadTestData();}@Testvoid createCustomerTest() {var input = new Customer();input.setName("name");input.setEmail("me@email.com");input.setAddresses(List.of(new Address() //.setStreet("street").setCity("city")));var customer = customerService.createCustomer(input);assertNotNull(customer.getId());assertTrue(customerRepository.containsEntity(customer));}@Testvoid getCustomerTest() {var id = 1L; // TODO fill idvar customer = customerService.getCustomer(id);assertTrue(customer.isPresent());}@Testvoid updateCustomerTest() {var id = 1L; // TODO fill idvar input = new Customer();input.setName("name");input.setEmail("me@email.com");input.setAddresses(List.of(new Address() //.setStreet("street").setCity("city")));assertTrue(customerRepository.containsKey(id));var customer = customerService.updateCustomer(id, input);assertTrue(customer.isPresent());assertTrue(customerRepository.containsEntity(customer.get()));}@Testvoid deleteCustomerTest() {var id = 1L; // TODO fill idassertTrue(customerRepository.containsKey(id));customerService.deleteCustomer(id);assertFalse(customerRepository.containsKey(id));}@Testvoid searchCustomersTest() {var searchCriteria = new CustomerSearchCriteria();var results = customerService.searchCustomers(searchCriteria, PageRequest.of(0, 10));Assertions.assertNotNull(results);Assertions.assertFalse(results.isEmpty());}}
This CustomerServiceTest
is just an skeleton, you will need to provide testing data and assertions.
Unit and Integration Tests
ZenWave SDK generates a complete suit of both Unit and Integration Tests:
Core Integration Tests
BackendApplicationDefaultPlugin
generates Integration Tests for infrastructure
or outbound adapter classes
(repositories in this case) that run as @SpringBootTest
against TestContainers using docker-compose.yml configuration.
These tests verify that JPA configuration and entity relationships are correctly mapped. As the developer, you need to populate tests with input data and assertions, and provide initial database state in the standard src/test/resources/data.sql
file. These integration tests are transactional, so the database state is rolled back to its original state after each test.
Testing JPA and relationshipst in CustomerRepositoryIntegrationTest.java
@Testvoid saveTest() {Customer customer = new Customer();customer.setName("Jane Smith");customer.setEmail("jane.smith@example.com");customer.setAddresses(List.of(new Address().setStreet("456 Elm St").setCity("Othertown")));// OneToMany paymentMethods owner: truevar paymentMethods = new PaymentMethod();paymentMethods.setType(PaymentMethodType.VISA);paymentMethods.setCardNumber("6543210987654321");customer.addPaymentMethods(paymentMethods);// Persist aggregate rootvar created = customerRepository.save(customer);// reloading to get relationships persisted by identityManager.flush();entityManager.refresh(created);Assertions.assertNotNull(created.getId());Assertions.assertNotNull(created.getVersion());Assertions.assertNotNull(created.getCreatedBy());Assertions.assertNotNull(created.getCreatedDate());Assertions.assertTrue(customer.getPaymentMethods().stream().allMatch(item -> item.getId() != null));}
Remember to verify that Services list in DockerComposeInitializer.java
match the services declared in docker-compose.yml
, pay particular attention to the database service name, which is app
in this case.
Services List in DockerComposeInitializer.java
private static final String DOCKER_COMPOSE_FILE = "./docker-compose.yml";private static final List<Service> SERVICES = List.of(new Service("postgresql", 5432, "DATASOURCE_URL", "jdbc:postgresql://%s:%s/app"),new Service("kafka", 9092, "KAFKA_BOOTSTRAP_SERVERS", "%s:%s"),new Service("schema-registry", 8081, "SCHEMA_REGISTRY_URL", "http://%s:%s"));
DockerComposeInitializer.java
contains the annotation required to start TestContainers in your @SpringBootTest
tests:
BaseRepositoryIntegrationTest.java
package io.zenwave360.example.infrastructure.jpa;import io.zenwave360.example.config.DockerComposeInitializer;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.ActiveProfiles;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)@ActiveProfiles("test")@DockerComposeInitializer.EnableDockerCompose@org.springframework.transaction.annotation.Transactionalpublic abstract class BaseRepositoryIntegrationTest {}
Core Unit Tests
BackendApplicationDefaultPlugin
also generates Unit Tests for your core services that use InMemory Repository implementations (avoiding Mockito). It also generates a ServicesInMemoryConfig.java
to provide service implementations wired with InMemory dependencies:
Using ServicesInMemoryConfig.java in Service Tests
/** Acceptance Test for CustomerService. */class CustomerServiceTest {private final Logger log = LoggerFactory.getLogger(getClass());ServicesInMemoryConfig context = new ServicesInMemoryConfig();CustomerServiceImpl customerService = context.customerService();CustomerRepositoryInMemory customerRepository = context.customerRepository();@BeforeEachvoid setUp() {context.reloadTestData();}
You can provide in-memory test data by creating JSON files in src/test/resources/{{persistence}}/data/{{aggregateName}}/{{id}}.json
- these will populate the in-memory repository before each test runs.
src/test/resources/data/jpa/customer/1.json
{"id": 1,"version": 1,"email": "john.doe@example.com","name": "John Doe","addresses": [{"city": "Anytown","street": "123 Main St"}],"paymentMethods": [{"type": 1,"cardNumber": "1234567890123456","createdBy": "system","createdDate": "2023-01-01T00:00:00","lastModifiedBy": "system","lastModifiedDate": "2023-01-01T00:00:00"}],"createdBy": "system","createdDate": "2023-01-01T00:00:00","lastModifiedBy": "system","lastModifiedDate": "2023-01-01T00:00:00"}
Exposing a REST API for your Service
Services can be decorated with @rest
, @get
, @post
, @put
, @delete
, @patch
annotations to document how they will be exposed as REST endpoints. The ZdlToOpenAPIPlugin
can generate a complete OpenAPI definition from these annotations, which you can then customize manually or by applying OpenAPI overlays during code generation.
The ZdlToOpenAPIPlugin
will generate #/components/schemas/**
for all entities, inputs and outputs used in your service commands. Additional annotations like @fileupload
and @filedownload
are available for file handling endpoints (see the Clinical Tool - Modulith example for details).
Create Customer Endpoint
When using the ZDLToOpenAPIPlugin
, the following
this service command:
@postcreateCustomer(Customer) Customer withEvents CustomerEvent
would produce this:
src/main/resources/public/apis/openapi.yml
/customers:post:operationId: createCustomerdescription: "createCustomer"tags: [Customer]requestBody:required: truecontent:application/json:schema:$ref: "#/components/schemas/Customer"responses:"201":description: "OK"content:application/json:schema:$ref: "#/components/schemas/Customer"
When using the OpenAPIControllersPlugin
, the following
src/main/resources/public/apis/openapi.yml
/customers:post:operationId: createCustomerdescription: "createCustomer"tags: [Customer]requestBody:required: truecontent:application/json:schema:$ref: "#/components/schemas/Customer"responses:"201":description: "OK"content:application/json:schema:$ref: "#/components/schemas/Customer"
would produce this java code:
CustomerApiController.java
@Overridepublic ResponseEntity<CustomerDTO> createCustomer(CustomerDTO reqBody) {log.debug("REST request to createCustomer: {}", reqBody);var input = mapper.asCustomer(reqBody);var customer = customerService.createCustomer(input);CustomerDTO responseDTO = mapper.asCustomerDTO(customer);return ResponseEntity.status(201).body(responseDTO);}
And its corresponding:
CustomerDTOsMapper.java Generated by ZenWave SDK
@Mapper(uses = BaseMapper.class)public interface CustomerDTOsMapper {CustomerDTOsMapper INSTANCE = Mappers.getMapper(CustomerDTOsMapper.class);// request mappingsCustomerSearchCriteria asCustomerSearchCriteria(CustomerSearchCriteriaDTO dto);Customer asCustomer(CustomerDTO dto);// response mappingsList<CustomerDTO> asCustomerDTOList(List<Customer> entityList);CustomerPaginatedDTO asCustomerPaginatedDTO(Page<Customer> page);default Page<CustomerDTO> asCustomerDTOPage(Page<Customer> page) {return page.map(this::asCustomerDTO);}CustomerDTO asCustomerDTO(Customer entity);}
CustomerApiControllerTest.java Generated by ZenWave SDK
@Testvoid createCustomerTest() {CustomerDTO reqBody = new CustomerDTO();reqBody.setName("John Doe");reqBody.setEmail("john.doe@example.com");reqBody.setAddresses(List.of(new AddressDTO("Anytown", "123 Main St")));reqBody.setPaymentMethods(List.of(new PaymentMethodDTO(PaymentMethodTypeDTO.VISA, "1234567890123456")));var response = controller.createCustomer(reqBody);Assertions.assertEquals(201, response.getStatusCode().value());}
Remember that generated
Mappers
andTests
are provided as a starting point, you will need to adapt them to your needs.
Search Customer Endpoint
When using the ZDLToOpenAPIPlugin
, the following
sevice command `@paginated` and returning an aray:
@post("/search")@paginatedsearchCustomers(CustomerSearchCriteria) Customer[]
would produce this:
src/main/resources/public/apis/openapi.yml
/customers/search:post:operationId: searchCustomersdescription: "searchCustomers"tags: [Customer]parameters:- $ref: "#/components/parameters/page"- $ref: "#/components/parameters/limit"- $ref: "#/components/parameters/sort"requestBody:required: truecontent:application/json:schema:$ref: "#/components/schemas/CustomerSearchCriteria"responses:"200":description: "OK"content:application/json:schema:$ref: "#/components/schemas/CustomerPaginated"
When using the OpenAPIControllersPlugin
, the previous OpenAPI definition:
src/main/resources/public/apis/openapi.yml
/customers/search:post:operationId: searchCustomersdescription: "searchCustomers"tags: [Customer]parameters:- $ref: "#/components/parameters/page"- $ref: "#/components/parameters/limit"- $ref: "#/components/parameters/sort"requestBody:required: truecontent:application/json:schema:$ref: "#/components/schemas/CustomerSearchCriteria"responses:"200":description: "OK"content:application/json:schema:$ref: "#/components/schemas/CustomerPaginated"
would produce this java code:
CustomerApiController.java
public ResponseEntity<Void> deleteCustomer(Long id) {log.debug("REST request to deleteCustomer: {}", id);customerService.deleteCustomer(id);return ResponseEntity.status(204).build();}@Overridepublic ResponseEntity<CustomerPaginatedDTO> searchCustomers(Integer page, Integer limit, List<String> sort, CustomerSearchCriteriaDTO reqBody) {log.debug("REST request to searchCustomers: {}, {}, {}, {}", page, limit, sort, reqBody);var input = mapper.asCustomerSearchCriteria(reqBody);var customerPage = customerService.searchCustomers(input, pageOf(page, limit, sort));var responseDTO = mapper.asCustomerPaginatedDTO(customerPage);return ResponseEntity.status(200).body(responseDTO);}
And its corresponding:
CustomerApiControllerTest.java Generated by ZenWave SDK
@Testvoid searchCustomersTest() {Optional<Integer> page = Optional.of(0);Optional<Integer> limit = Optional.of(10);Optional<List<String>> sort = Optional.of(List.of("name"));CustomerSearchCriteriaDTO reqBody = new CustomerSearchCriteriaDTO();var response = controller.searchCustomers(reqBody, page, limit, sort);Assertions.assertEquals(200, response.getStatusCode().value());}
Web Adapters Unit Tests
OpenAPIControllersPlugin
generates Spring MVC controllers that implement your OpenAPI specification, along with corresponding Unit Tests that use ServicesInMemoryConfig.java
for dependency injection.
Unit Testing Spring MVC Controllers
/** Test controller for CustomerApiController. */class CustomerApiControllerTest {private final Logger log = LoggerFactory.getLogger(getClass());ServicesInMemoryConfig context = new ServicesInMemoryConfig();CustomerApiController controller = new CustomerApiController(context.customerService());@BeforeEachvoid setUp() {context.reloadTestData();}@Testvoid createCustomerTest() {CustomerDTO reqBody = new CustomerDTO();reqBody.setName("John Doe");reqBody.setEmail("john.doe@example.com");reqBody.setAddresses(List.of(new AddressDTO("Anytown", "123 Main St")));reqBody.setPaymentMethods(List.of(new PaymentMethodDTO(PaymentMethodTypeDTO.VISA, "1234567890123456")));var response = controller.createCustomer(reqBody);Assertions.assertEquals(201, response.getStatusCode().value());}
You can disable controller Unit Test generation if you prefer to test your REST API through HTTP calls instead of testing the Java controllers directly.
Web Adapters Integration Tests with Spring WebTestClient
If you prefer to test your REST API via HTTP semantics you can use SpringWebTestClientPlugin
to generate both single endpoint tests as well as business flows spaning multiple endpoints:
When using the SpringWebTestClientPlugin
, the following
zenwave-scripts.zw
SpringWebTestClientPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}
would produce this API test:
CustomerApiIntegrationTest
package io.zenwave360.example.adapters.web;import io.zenwave360.example.adapters.web.model.*;import io.zenwave360.example.config.ServicesInMemoryConfig;import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.List;import java.util.Optional;/** Test controller for CustomerApiController. */class CustomerApiControllerTest {private final Logger log = LoggerFactory.getLogger(getClass());ServicesInMemoryConfig context = new ServicesInMemoryConfig();CustomerApiController controller = new CustomerApiController(context.customerService());@BeforeEachvoid setUp() {context.reloadTestData();}@Testvoid createCustomerTest() {CustomerDTO reqBody = new CustomerDTO();reqBody.setName("John Doe");reqBody.setEmail("john.doe@example.com");reqBody.setAddresses(List.of(new AddressDTO("Anytown", "123 Main St")));reqBody.setPaymentMethods(List.of(new PaymentMethodDTO(PaymentMethodTypeDTO.VISA, "1234567890123456")));var response = controller.createCustomer(reqBody);Assertions.assertEquals(201, response.getStatusCode().value());}@Testvoid getCustomerTest() {Long id = 1L;var response = controller.getCustomer(id);Assertions.assertEquals(200, response.getStatusCode().value());}@Testvoid updateCustomerTest() {Long id = 1L;CustomerDTO reqBody = new CustomerDTO();reqBody.setName("John Doe");reqBody.setEmail("john.doe@example.com");reqBody.setAddresses(List.of(new AddressDTO("Anytown", "123 Main St")));reqBody.setPaymentMethods(List.of(new PaymentMethodDTO(PaymentMethodTypeDTO.VISA, "1234567890123456")));var response = controller.updateCustomer(id, reqBody);Assertions.assertEquals(200, response.getStatusCode().value());}@Testvoid deleteCustomerTest() {Long id = 1L;var response = controller.deleteCustomer(id);Assertions.assertEquals(204, response.getStatusCode().value());}@Testvoid searchCustomersTest() {Optional<Integer> page = Optional.of(0);Optional<Integer> limit = Optional.of(10);Optional<List<String>> sort = Optional.of(List.of("name"));CustomerSearchCriteriaDTO reqBody = new CustomerSearchCriteriaDTO();var response = controller.searchCustomers(reqBody, page, limit, sort);Assertions.assertEquals(200, response.getStatusCode().value());}}
When using the SpringWebTestClientPlugin
, the following
zenwave-scripts.zw
SpringWebTestClientPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"groupBy businessFlowbusinessFlowTestName CreateUpdateDeleteCustomerIntegrationTestoperationIds createCustomer,updateCustomer,deleteCustomer,getCustomer}OpenAPIKaratePlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}
would produce this Business Flow API test:
CustomerApiIntegrationTest
package io.zenwave360.example.adapters.web;import io.zenwave360.example.adapters.web.model.CustomerDTO;import io.zenwave360.example.adapters.web.model.PaymentMethodTypeDTO;import org.junit.jupiter.api.Test;import org.springframework.http.MediaType;import static org.springframework.http.HttpMethod.*;/*** Business Flow Test for: createCustomer, getCustomer, updateCustomer, deleteCustomer.*/class CreateUpdateDeleteCustomerIntegrationTest extends BaseWebTestClientTest {/*** Business Flow Test for: createCustomer, getCustomer, updateCustomer, deleteCustomer.*/@Testvoid testCreateUpdateDeleteCustomerIntegrationTest() {// createCustomer: createCustomervar customerRequestBody0 = """{"email": "jane.doe@example.com","name": "Jane Doe","addresses": [{"city": "Othertown","street": "456 Elm St"}],"paymentMethods": [{"type": "VISA","cardNumber": "6543210987654321"}]}""";var createCustomerResponse0 = webTestClient.method(POST).uri("/api/customers").accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON).bodyValue(customerRequestBody0).exchange().expectStatus().isEqualTo(201).expectHeader().contentType(MediaType.APPLICATION_JSON).returnResult(CustomerDTO.class);// getCustomer: getCustomervar id = createCustomerResponse0.getResponseBody().blockFirst().getId();var getCustomerResponse1 = webTestClient.method(GET).uri("/api/customers/{id}", id).accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isEqualTo(200).expectHeader().contentType(MediaType.APPLICATION_JSON).returnResult(CustomerDTO.class);// updateCustomer: updateCustomerCustomerDTO customerRequestBody2 = getCustomerResponse1.getResponseBody().blockFirst();customerRequestBody2.setName("updated");customerRequestBody2.setEmail("updated@email.com");customerRequestBody2.getPaymentMethods().get(0).setType(PaymentMethodTypeDTO.VISA);var updateCustomerResponse2 = webTestClient.method(PUT).uri("/api/customers/{id}", id).accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON).bodyValue(customerRequestBody2).exchange().expectStatus().isEqualTo(200).expectHeader().contentType(MediaType.APPLICATION_JSON).returnResult(CustomerDTO.class);// deleteCustomer: deleteCustomerwebTestClient.method(DELETE).uri("/api/customers/{id}", id).accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isEqualTo(204);// getCustomer: getCustomer (not found)webTestClient.method(GET).uri("/api/customers/{id}", id).accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isEqualTo(404);}}
You can control whether these are Unit or Integration Tests with @SpringBootTest
in
BaseWebTestClientTest.java
@ActiveProfiles("test")@DockerComposeInitializer.EnableDockerCompose@org.springframework.transaction.annotation.Transactionalpublic abstract class BaseWebTestClientTest {@Autowiredprotected WebApplicationContext context;protected WebTestClient webTestClient;@BeforeEachvoid setup() {this.webTestClient = MockMvcWebTestClient.bindToApplicationContext(this.context).build();}}
As always, it's your responsibility as the developer to provide test data and parameters that match the initial state, whether you're using a containerized database or an in-memory implementation.
Publishing Domain Events with AsyncAPI
You service commands can publish Domain Events as part of their operative using withEvent
keyword:
When using withEvents
in service commands
zenwave-model.zdl
updateCustomer(id, Customer) Customer? withEvents CustomerEvent
would produce generate and use an EventsProvider in your service implementation:
CustomerServiceImpl.java
@Transactionalpublic Optional<Customer> updateCustomer(Long id, Customer input) {log.debug("Request updateCustomer: {} {}", id, input);var customer = customerRepository.findById(id).map(existingCustomer -> {return customerServiceMapper.update(existingCustomer, input);}).map(customerRepository::save);if (customer.isPresent()) {// emit eventsvar customerEvent = eventsMapper.asCustomerEvent(customer.get());eventsProducer.onCustomerEvent(customerEvent);}return customer;}
You can control whether you want to keep you Domain Events internal to your application or publish them to an external broker and documented using AsyncAPI:
Domain Events decorated with @asyncapi in ZDL
@copy(Customer)@asyncapi({ channel: "CustomersChannel", topic: "customers" })event CustomerEvent {id Long requiredversion Integer// all fields from Customer are copied here, but not relationshipspaymentMethods PaymentMethod[]}@asyncapi({ channel: "CustomersChannel", topic: "customers" })event CustomerDeletedEvent {id Long required}
Events decorated with @asyncapi
when running ZDLToAsyncAPIPlugin
zenwave-scripts.zw
ZDLToAsyncAPIPlugin {asyncapiVersion v3schemaFormat avroavroPackage "io.zenwave360.example.core.outbound.events.dtos"idType longtargetFile "src/main/resources/public/apis/asyncapi.yml"includeKafkaCommonHeaders true}
would generate a complete asyncapi.yml file (in this case, based in plugin configuration, including avro schemas avro/*.avsc
)
src/main/resources/public/apis/asyncapi.yml
asyncapi: 3.0.0info:title: "ZenWave Customer JPA Example"version: 0.0.1tags:- name: "Default"- name: "Customer"defaultContentType: application/jsonchannels:CustomersChannel:address: "customers"messages:CustomerDeletedEventMessage:$ref: '#/components/messages/CustomerDeletedEventMessage'CustomerEventMessage:$ref: '#/components/messages/CustomerEventMessage'operations:onCustomerEvent:action: sendtags:- name: Customerchannel:$ref: '#/channels/CustomersChannel'onCustomerDeletedEvent:action: sendtags:- name: Customerchannel:$ref: '#/channels/CustomersChannel'components:messages:CustomerEventMessage:name: CustomerEventMessagetitle: ""summary: ""traits:- $ref: '#/components/messageTraits/CommonHeaders'payload:schemaFormat: application/vnd.apache.avro+json;version=1.9.0schema:$ref: "avro/CustomerEvent.avsc"CustomerDeletedEventMessage:name: CustomerDeletedEventMessagetitle: ""summary: ""traits:- $ref: '#/components/messageTraits/CommonHeaders'payload:schemaFormat: application/vnd.apache.avro+json;version=1.9.0schema:$ref: "avro/CustomerDeletedEvent.avsc"messageTraits:CommonHeaders:headers:type: objectproperties:kafka_messageKey:type: "long"description: This header value will be populated automatically at runtimex-runtime-expression: $message.payload#/id
Then you can configure ZenWave SDK Maven Plugin to generate all required DTOs and the EventProducer
we see before referenced in you CustomerService.java
ZenWave SDK Maven Plugin configured to genreate Spring Cloud Stream code from asyncapi.yml
<plugin><groupId>io.zenwave360.sdk</groupId><artifactId>zenwave-sdk-maven-plugin</artifactId><version>${zenwave.version}</version><configuration><inputSpec>${project.basedir}/src/main/resources/public/apis/asyncapi.yml</inputSpec><skip>false</skip><addCompileSourceRoot>true</addCompileSourceRoot><addTestCompileSourceRoot>true</addTestCompileSourceRoot></configuration><executions><!-- DTOs --><!-- <execution>--><!-- <id>generate-asyncapi-dtos</id>--><!-- <phase>generate-sources</phase>--><!-- <goals>--><!-- <goal>generate</goal>--><!-- </goals>--><!-- <configuration>--><!-- <generatorName>jsonschema2pojo</generatorName>--><!-- <configOptions>--><!-- <modelPackage>${asyncApiModelPackage}</modelPackage>--><!-- <jsonschema2pojo.isUseJakartaValidation>true</jsonschema2pojo.isUseJakartaValidation>--><!-- <jsonschema2pojo.useLongIntegers>true</jsonschema2pojo.useLongIntegers>--><!-- <jsonschema2pojo.includeAdditionalProperties>true</jsonschema2pojo.includeAdditionalProperties>--><!-- </configOptions>--><!-- </configuration>--><!-- </execution>--><!-- Generate PROVIDER --><execution><id>generate-asyncapi</id><phase>generate-sources</phase><goals><goal>generate</goal></goals><configuration><generatorName>spring-cloud-streams3</generatorName><configOptions><role>provider</role><style>imperative</style><transactionalOutbox>modulith</transactionalOutbox><includeApplicationEventListener>true</includeApplicationEventListener><modelPackage>${asyncApiModelPackage}</modelPackage><producerApiPackage>${asyncApiProducerApiPackage}</producerApiPackage><consumerApiPackage>${asyncApiConsumerApiPackage}</consumerApiPackage></configOptions></configuration></execution></executions><dependencies><dependency><groupId>io.zenwave360.sdk.plugins</groupId><artifactId>asyncapi-spring-cloud-streams3</artifactId><version>${zenwave.version}</version></dependency><dependency><groupId>io.zenwave360.sdk.plugins</groupId><artifactId>asyncapi-jsonschema2pojo</artifactId><version>${zenwave.version}</version></dependency></dependencies></plugin>
Because we are using avro as payload format, we also need to configure avro-maven-plugin to generate java classes from our avro schemas:
avro-maven-plugin configured to generate java classes from avro schemas
<plugin><groupId>org.apache.avro</groupId><artifactId>avro-maven-plugin</artifactId><version>1.11.1</version><executions><execution><goals><goal>schema</goal></goals><phase>generate-sources</phase></execution></executions><configuration><sourceDirectory>${project.basedir}/src/main/resources/public/apis/avro</sourceDirectory><outputDirectory>${project.basedir}/target/generated-sources/avro</outputDirectory><imports><import>${project.basedir}/src/main/resources/public/apis/avro/PaymentMethodType.avsc</import><import>${project.basedir}/src/main/resources/public/apis/avro/PaymentMethod.avsc</import><import>${project.basedir}/src/main/resources/public/apis/avro/Address.avsc</import></imports></configuration></plugin>
Please refer to https://www.zenwave360.io/zenwave-sdk/plugins/asyncapi-spring-cloud-streams3/ for more details about how to configure ZenWave SDK Maven Plugin.
Running the Customer Service
Prerequisites
- JDK 21+
- Maven 3.8+
- Docker & Docker Compose - If you don't have Docker Compose installed, we recommend Rancher Desktop configured with
dockerd
engine (notcontainerd
), which includes bothdocker
anddocker-compose
commands. - Your favorite IDE
Quick Start
Follow these steps to run the complete application:
-
Start infrastructure services:
docker-compose up -d -
Run the Spring Boot application:
mvn spring-boot:run -
Access the API:
- Open Swagger UI in your browser
- Use Basic Authentication: username
admin
, passwordpassword
-
Test the endpoints:
- Try creating a customer via the POST
/customers
endpoint - Retrieve customers using GET
/customers/search
- Try creating a customer via the POST
What's Running
- PostgreSQL (port 5432) - Customer data persistence
- Kafka (port 9092) - Domain events messaging
- Spring Boot App (port 8080) - REST API and business logic
Happy Coding! π