Let's get started with a Microservice Architecture with Spring Cloud:
Introduction to Apache Causeway
Last updated: July 25, 2026
1. Overview
Apache Causeway is a framework for building domain-driven applications on top of Spring Boot. Instead of beginning with controllers and pages, we describe domain objects, their state, behavior, and business rules. Causeway turns that description into a metamodel at runtime.
After that, the Wicket viewer and RESTful Objects viewer use the same metamodel to provide a web UI and a hypermedia API. This makes Causeway particularly useful for internal management software, where broad coverage of the domain and fast feedback often matter more than a custom interface.
In this tutorial, we’ll use Apache Causeway 3.6.0, Java 21, and Maven to build a small asset management application. Specifically, we’ll model assets such as laptops, monitors, and phones, add lifecycle actions and business rules, run the generated UI, and invoke one of the generated REST endpoints.
2. Mental Model
The domain is the business area that the software addresses.
2.1. Domain
The domain model is the set of types, relationships, operations, and rules that represent that area.
In this case, the domain is the management of company hardware:
- which assets exist
- who has them
- which lifecycle transitions are valid
So, the main domain object here is Asset. Because instances are stored in a database, Asset is also a domain entity. A view model, by contrast, could represent temporary or calculated information without having a persistent identity. Causeway exposes scalar values such as type, serialNumber, status, and assignedTo as properties. A relationship to several other objects would be a collection.
Behavior appears as actions such as assignTo(), returnToInventory(), and retire(). Operations that don’t naturally belong to one asset, such as creating or finding assets, live in the Assets domain service. Causeway uses these domain members as the common vocabulary for both its UI and REST viewers.
2.2. Causeway vs. Traditional Spring MVC Application
In a traditional Spring MVC application, a request usually reaches a controller, which performs several actions:
- calls an application service
- prepares a model or DTO
- selects a view or produces a response
Even when the business logic is well isolated, each new use case generally needs explicit web-layer code.
Causeway changes the starting point. It introspects entities, domain services, annotations, and method signatures, and records them in its metamodel. The viewers render the properties, collections, actions, parameters, and validation messages that the metamodel describes. Therefore, we can add a basic use case without creating a matching controller, form, and template.
This doesn’t remove the rest of the application architecture. We still configure Spring modules, persistence, security, and menu layout, and we can place complex orchestration in application services when appropriate. Causeway reduces presentation-layer repetition by making the domain model drive the interaction.
2.3. When Causeway Is a Good Fit
This model works well for back-office tools, administration systems, internal management applications, and domain-heavy prototypes. Such systems often combine structured data with many business operations, so a consistent generated interface lets a team validate terminology, workflows, and rules early.
It’s a weaker fit when the primary requirement is a highly branded consumer experience, pixel-level interaction design, or a page flow that doesn’t map naturally to domain objects. In such cases, we can place a custom client in front of the Causeway APIs, but doing so sacrifices some of the development speed offered by the generic viewer.
The common-use-case documentation of the framework presents the same spectrum, from prototyping and generic line-of-business UIs to custom clients. The important decision is whether domain coverage or presentation control is the stronger requirement.
3. Building an Internal Asset Management Application
Let’s model an asset with several characteristics:
- type
- serial number
- status
- assignee (optional)
The normal asset lifecycle is AVAILABLE to ASSIGNED, then back to AVAILABLE, while an available asset can also become RETIRED.
A runnable Causeway application needs framework module imports, persistence, security, and menu configuration in addition to the central domain code. To follow along, let’s keep the complete project on GitHub open. The snippets that follow cover the decisions relevant to the tutorial, while the repository contains the supporting bootstrap and configuration files.
3.1. Project Setup
To begin with, we need JDK 21 and Maven 3.9.11. The Causeway application starter parent manages the compatible Spring Boot and framework dependency versions. In pom.xml, let’s add the web application bundle, both viewers, Simple Security, JPA with EclipseLink, and an H2 in-memory database:
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.causeway.app</groupId>
<artifactId>causeway-app-starter-parent</artifactId>
<version>3.6.0</version>
<relativePath/>
</parent>
<groupId>com.baeldung</groupId>
<artifactId>apache-causeway</artifactId>
<version>1.0.0-SNAPSHOT</version>
<!-- The name and description are omitted. -->
<dependencies>
<dependency>
<groupId>org.apache.causeway.mavendeps</groupId>
<artifactId>causeway-mavendeps-webapp</artifactId>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.apache.causeway.viewer</groupId>
<artifactId>causeway-viewer-wicket-viewer</artifactId>
</dependency>
<dependency>
<groupId>org.apache.causeway.viewer</groupId>
<artifactId>causeway-viewer-restfulobjects-jaxrsresteasy</artifactId>
</dependency>
<dependency>
<groupId>org.apache.causeway.security</groupId>
<artifactId>causeway-security-simple</artifactId>
</dependency>
<dependency>
<groupId>org.apache.causeway.persistence</groupId>
<artifactId>causeway-persistence-jpa-eclipselink</artifactId>
</dependency>
<dependency>
<groupId>org.apache.causeway.viewer</groupId>
<artifactId>causeway-viewer-wicket-applib</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
</dependency>
<!-- JUnit, Mockito, and Causeway test dependencies are omitted. -->
</dependencies>
<!-- EclipseLink weaving and Spring Boot packaging plugins are omitted. -->
<properties>
<java.version>21</java.version>
<maven.compiler.release>21</maven.compiler.release>
</properties>
</project>
The web application bundle supplies the common runtime dependencies. The viewer artifacts add the generated Wicket interface and RESTful Objects API, while Simple Security and H2 keep this example self-contained.
Next, a small Spring configuration class identifies the application code:
@Configuration
@ComponentScan(basePackageClasses = Assets.class)
@EnableJpaRepositories(basePackageClasses = AssetRepository.class)
@EntityScan(basePackageClasses = Asset.class)
public class AssetManagementModule {
}
Let’s break it down:
- @ComponentScan discovers the domain service
- @EnableJpaRepositories enables the Spring Data repository
- @EntityScan registers the JPA entity
Then, we import this configuration together with the required Causeway modules in AppManifest:
@Configuration
@Import({
CausewayModuleApplibMixins.class,
CausewayModuleCoreRuntimeServices.class,
CausewayModuleSecuritySimple.class,
CausewayModulePersistenceJpaEclipselink.class,
CausewayModuleViewerRestfulObjectsJaxrsResteasy.class,
CausewayModuleViewerWicketApplibMixins.class,
CausewayModuleViewerWicketViewer.class,
AssetManagementModule.class
})
@PropertySource(CausewayPresets.NoTranslations)
public class AppManifest {
// Password encoding and the demo-only SimpleRealm configuration are omitted
}
Importing a module makes its Spring beans and Causeway features available to the application.
The repository also configures H2, EclipseLink schema creation, the demo user, and the menu layout. In application.yml, we set causeway.applib.annotation.action.explicit to true, so only methods explicitly annotated with @Action become actions.
3.2. Creating the Asset Domain Entity
Let’s define the persistent type and give it a stable logical name:
@Entity
@Table(
schema = "assets",
name = "Asset",
uniqueConstraints = @UniqueConstraint(
name = "Asset__serialNumber__UNQ",
columnNames = "serial_number"
)
)
@EntityListeners(CausewayEntityListener.class)
@Named("assets.Asset")
@DomainObject
@DomainObjectLayout
public class Asset implements Comparable<Asset> {
protected Asset() {
}
public Asset(final AssetType type, final String serialNumber) {
this.type = type;
this.serialNumber = serialNumber;
this.status = AssetStatus.AVAILABLE;
}
// The identifier, version, comparison helpers, and members discussed below are omitted
}
So, let’s take a closer look at what the code means:
- The JPA annotations map the entity and enforce a database-level uniqueness constraint on serial_number.
- @Named supplies the logical identifier that Causeway uses independently of the Java package name.
- @DomainObject explicitly identifies the class as a Causeway domain object.
- CausewayEntityListener connects JPA lifecycle events to Causeway, including service injection and lifecycle notifications.
- The public constructor establishes the first lifecycle invariant: every new asset begins in the AVAILABLE status.
JPA maps the stored values, while Causeway recognizes the corresponding getters as properties. To be clear, let’s see the identity properties:
@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false, length = 20)
private AssetType type;
@PropertyLayout(fieldSetId = LayoutConstants.FieldSetId.IDENTITY, sequence = "1")
public AssetType getType() {
return type;
}
@Column(name = "serial_number", nullable = false, length = 80)
private String serialNumber;
@Title(prepend = "Asset: ")
@Property(maxLength = 80)
@PropertyLayout(fieldSetId = LayoutConstants.FieldSetId.IDENTITY, sequence = "2")
public String getSerialNumber() {
return serialNumber;
}
// Status and assignedTo are omitted here to focus on the identity properties
There are several important details:
- EnumType.STRING stores values such as LAPTOP instead of fragile ordinal numbers.
- @PropertyLayout groups and orders members in the generated UI.
- @Title makes the serial number part of the object’s display title.
The remaining properties hold an AssetStatus and an optional employee name.
3.3. Adding Domain Behavior With Actions
Notably, we don’t want callers to change lifecycle fields independently through setters. Instead, the entity exposes operations that update related state together.
First, the assignment action records an employee and changes the status:
@Action(semantics = SemanticsOf.IDEMPOTENT)
@ActionLayout(
fieldSetId = LayoutConstants.FieldSetId.DETAILS,
position = ActionLayout.Position.PANEL,
describedAs = "Assigns an available asset to an employee"
)
public Asset assignTo(
@Parameter(maxLength = 100)
@ParameterLayout(named = "Employee") final String employee) {
assignedTo = employee.trim();
status = AssetStatus.ASSIGNED;
return this;
}
Causeway renders assignTo() as an action and derives its prompt from the parameter metadata. By returning this, it tells the viewer to continue with the updated asset. The IDEMPOTENT semantic describes the intended invocation semantics, but it doesn’t make the Java implementation idempotent automatically.
The other lifecycle actions return an asset to inventory or retire it:
@Action(semantics = SemanticsOf.IDEMPOTENT)
@ActionLayout(
fieldSetId = LayoutConstants.FieldSetId.DETAILS,
position = ActionLayout.Position.PANEL,
describedAs = "Returns an assigned asset to inventory"
)
public Asset returnToInventory() {
assignedTo = null;
status = AssetStatus.AVAILABLE;
return this;
}
@Action(semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE)
@ActionLayout(
fieldSetId = LayoutConstants.FieldSetId.DETAILS,
position = ActionLayout.Position.PANEL,
describedAs = "Permanently retires an asset"
)
public Asset retire() {
assignedTo = null;
status = AssetStatus.RETIRED;
return this;
}
Specifically, returnToInventory() clears the assignee as it restores AVAILABLE. Retirement is deliberately explicit, and IDEMPOTENT_ARE_YOU_SURE asks the Wicket viewer for confirmation.
At this point the methods express the state changes, but we still need to control when each change is valid.
3.4. Adding Business Rules
Causeway associates supporting methods with domain members by naming convention. For example, disableAssignTo() controls the availability of assignTo(), while validate0AssignTo() validates parameter zero, which is the first parameter:
@MemberSupport
public String disableAssignTo() {
return status == AssetStatus.AVAILABLE
? null
: "Only available assets can be assigned";
}
@MemberSupport
public String validate0AssignTo(final String employee) {
return employee == null || employee.isBlank()
? "Employee name is required"
: null;
}
@MemberSupport
public String disableReturnToInventory() {
return status == AssetStatus.ASSIGNED
? null
: "Only assigned assets can be returned";
}
@MemberSupport
public String disableRetire() {
if (status == AssetStatus.ASSIGNED) {
return "Return the asset before retiring it";
}
return status == AssetStatus.RETIRED
? "The asset is already retired"
: null;
}
Here, null result permits the interaction. A message disables or rejects it and gives the viewer or API client a reason. Consequently, only an available asset can be assigned, only an assigned asset can be returned, and an assigned asset must be returned before retirement.
@MemberSupport also lets Causeway validate that a supporting method still matches an existing domain member.
String parameters are mandatory by default, so the framework rejects an empty Employee field before calling the action. The validator additionally handles whitespace-only input. These checks apply to Causeway-managed interactions; a direct Java call to assignTo() remains an ordinary method call and doesn’t invoke the supporting methods automatically.
The business rules are part of the domain interaction, so both generated viewers can enforce them without duplicating conditions in a controller or page.
3.5. Creating an Asset Domain Service
Entity actions operate on one existing asset. Creation and queries belong in a domain service backed by a Spring Data JPA repository:
@Named("assets.Assets")
@DomainService
@Priority(PriorityPrecedence.EARLY)
public class Assets {
private final RepositoryService repositoryService;
private final AssetRepository assetRepository;
@Inject
public Assets(
final RepositoryService repositoryService,
final AssetRepository assetRepository) {
this.repositoryService = repositoryService;
this.assetRepository = assetRepository;
}
@Action(semantics = SemanticsOf.NON_IDEMPOTENT)
@ActionLayout(promptStyle = PromptStyle.DIALOG_MODAL)
public Asset create(
@ParameterLayout(named = "Type") final AssetType type,
@Parameter(maxLength = 80)
@ParameterLayout(named = "Serial number") final String serialNumber) {
return repositoryService.persist(new Asset(type, serialNumber.trim()));
}
@MemberSupport
public String validate1Create(final String serialNumber) {
if (serialNumber == null || serialNumber.isBlank()) {
return "Serial number is required";
}
return assetRepository.findBySerialNumberIgnoreCase(serialNumber.trim()).isPresent()
? "An asset with this serial number already exists"
: null;
}
// listAll() appears in the API section; findBySerialNumber() is in the complete project
}
@DomainService includes the service in the metamodel, @Named assigns the logical name used by the REST viewer, and @Priority places it early in Spring’s ordering.
RepositoryService persists a new entity through the Causeway abstraction, whereas AssetRepository provides application-specific queries.
The repository itself uses Spring Data-derived queries, so it doesn’t need an implementation class:
public interface AssetRepository extends JpaRepository<Asset, Long> {
List<Asset> findAllByOrderBySerialNumberAsc();
List<Asset> findBySerialNumberContainingIgnoreCaseOrderBySerialNumberAsc(
String serialNumber
);
Optional<Asset> findBySerialNumberIgnoreCase(String serialNumber);
}
The method names describe the ordering, partial match, and case-insensitive exact match that Spring Data generates. This keeps persistence queries separate from the Causeway-facing service actions.
The supporting method name validate1Create() refers to parameter one, the second parameter of create(). It rejects blank serial numbers and existing case-insensitive duplicates before persistence. The database constraint additionally protects against concurrent exact-value duplicates. Enforcing case-insensitive uniqueness at the database level would require normalization, a suitable index, or a case-insensitive collation.
Finally, menubars.layout.xml explicitly places the three service actions in the Assets menu:
<!-- The menuBars root, namespaces, primary container, and non-Assets menus are omitted. -->
<mb3:menu>
<mb3:named>Assets</mb3:named>
<mb3:section>
<mb3:serviceAction objectType="assets.Assets" id="create"/>
<mb3:serviceAction objectType="assets.Assets" id="findBySerialNumber"/>
<mb3:serviceAction objectType="assets.Assets" id="listAll"/>
</mb3:section>
</mb3:menu>
The objectType value matches the logical name of the service, and each id matches an action method. @DomainService registers the service in the metamodel, each @Action exposes a method, and the layout file determines this menu grouping.
3.6. Entry Point
The application entry point activates the Causeway prototyping preset and then starts Spring Boot:
@SpringBootApplication
@Import(AppManifest.class)
public class AssetManagementApplication extends SpringBootServletInitializer {
public static void main(final String[] args) {
CausewayPresets.prototyping();
SpringApplication.run(AssetManagementApplication.class, args);
}
}
So, let’s run the application.
4. Running the Internal Asset Management Application
From the apache-causeway module, we can build and run the application:
mvn clean install
mvn spring-boot:run
Next, let’s open http://localhost:8080/wicket/ and sign in with the demo credentials admin and pass.
4.1. Demo Interaction
From the Assets menu, let’s choose Create, select Laptop, and enter LT-001. Causeway immediately renders the new object using its title, properties, field sets, and actions:
When we open Assign To and submit an empty employee, the mandatory parameter prevents the invocation, and the generated prompt displays the validation error:
After entering Alice, the action changes the asset to ASSIGNED. The same page now enables Return To Inventory and disables Assign To and Retire, reflecting the supporting methods without page-specific conditional code:
No asset-specific controller, form, or HTML template was required for this generic UI.
The prototyping preset, in-memory H2 database, automatic schema creation, and SimpleRealm credentials are appropriate only for this local example. A production application should use durable persistence, schema migrations, production security, and a production-ready Causeway configuration.
4.2. Generated API
The service marks its list operation as safe:
@Action(semantics = SemanticsOf.SAFE)
public List<Asset> listAll() {
return assetRepository.findAllByOrderBySerialNumberAsc();
}
The RESTful Objects viewer maps this action to an authenticated GET. With the application still running, we can invoke it using the demo account:
$ curl -i -u admin:pass \
-H 'Accept: application/json' \
'http://localhost:8080/restful/services/assets.Assets/actions/listAll/invoke'
The URL contains the logical service name assets.Assets and the action identifier listAll. The verified invocation returns 200 OK and reaches the domain service rather than exposing AssetRepository directly.
The response isn’t a raw JSON array. It is a RESTful Objects DomainObjectList representation containing links, relation types, and media-type profiles that allow a client to navigate the domain.
This is useful for generic clients and integrations, but before treating it as a public, versioned API, we should deliberately restrict the exposed surface or define a client-specific representation.
5. Conclusion
In this article, we began with the Causeway domain-first mental model and built an internal asset management application. Specifically, we mapped a JPA entity, expressed lifecycle behavior as actions, enforced valid transitions with supporting methods, and added a domain service for creation and queries.
After that, we ran the generated Wicket UI and invoked the RESTful Objects API. Causeway is a strong option when a behavior-rich domain and rapid delivery of consistent management screens outweigh the need for a fully custom interface.
The complete source code is available over on GitHub.
















