eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

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:

An available asset with the actions derived from its domain model.

When we open Assign To and submit an empty employee, the mandatory parameter prevents the invocation, and the generated prompt displays the validation error:

The generated action prompt rejects a missing employee.

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:

Assigned asset - The generated UI reflects the asset's new state and available transitions.

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.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)
guest
0 Comments
Oldest
Newest
Inline Feedbacks
View all comments