Skip to content

How to Use Maven Plugins for Spring Boot CRUD Code Generation

Purpose

This post demonstrates how to automate Spring Boot CRUD code generation using Maven plugins, specifically the Spring CRUD Generator plugin.

When I started building Spring Boot applications, I spent hours writing the same repetitive code for each entity: entity classes, DTOs, mappers, services, controllers, repository interfaces, database migrations, API documentation, and Docker configuration. I wanted to find a way to generate this code from configuration instead of writing it manually.

Environment

  • Spring Boot 3.2
  • Maven 3.9
  • Java 17
  • MySQL 8.0 / PostgreSQL 14
  • Spring CRUD Generator 1.3.0

The Problem: CRUD Boilerplate Bloat

When I build a Spring Boot CRUD application, I need to create multiple layers for each entity:

Entity → DTO → Mapper → Service → Controller → Repository → Migration → API Docs → Docker

For a simple User entity, I used to write:

  1. User entity with JPA annotations (50+ lines)
  2. UserDTO request/response classes (30+ lines)
  3. UserMapper interface with MapStruct (20+ lines)
  4. UserService interface and implementation (80+ lines)
  5. UserController with REST endpoints (60+ lines)
  6. UserRepository interface (10+ lines)
  7. Flyway migration SQL file (20+ lines)
  8. OpenAPI annotations on controller (15+ lines)
  9. Docker configuration in Dockerfile (30+ lines)

That’s over 300 lines of code per entity, most of it following the same pattern.

When I have 10 entities, that’s 3,000+ lines of repetitive code. If I make a mistake in one layer, I get runtime errors. If I want to change the architecture pattern, I have to update all 10 entities manually.

The Solution: Spring CRUD Generator

Spring CRUD Generator is a Maven plugin that generates all these layers from a single YAML or JSON configuration file. I define my data model once, and the plugin generates consistent code following Spring Boot best practices.

The plugin generates:

  • JPA entities with validation annotations
  • Request/response DTOs
  • MapStruct mappers for entity-DTO conversion
  • Service interfaces and implementations
  • REST controllers with OpenAPI annotations
  • Spring Data JPA repositories
  • Flyway database migrations
  • Dockerfile and docker-compose configuration

Configuration File

I create a file called spring-crud-generator.yml in my project root:

spring-crud-generator.yml
project:
name: "User Management API"
package: "com.example.usermanagement"
database: "postgresql"
entities:
- name: "User"
table: "users"
fields:
- name: "id"
type: "Long"
primaryKey: true
generated: true
- name: "username"
type: "String"
nullable: false
unique: true
length: 50
- name: "email"
type: "String"
nullable: false
unique: true
length: 100
- name: "createdAt"
type: "LocalDateTime"
nullable: false
insertable: false
updatable: false
options:
generateOpenApi: true
generateFlyway: true
generateDocker: true
excludeNullsInResponse: true

I can explain the key parts:

  • project: Defines package structure and database type
  • entities: List of entities to generate
  • fields: Column definitions with constraints
  • options: Toggle OpenAPI, Flyway, Docker generation
  • excludeNullsInResponse: Omits null fields in JSON responses (v1.3.0 feature)

Maven Plugin Configuration

I add the plugin to my pom.xml:

pom.xml
<build>
<plugins>
<plugin>
<groupId>com.github.bwenskie</groupId>
<artifactId>spring-crud-generator-maven-plugin</artifactId>
<version>1.3.0</version>
<configuration>
<configFile>spring-crud-generator.yml</configFile>
<outputDir>${project.basedir}/src/main/java</outputDir>
</configuration>
</plugin>
</plugins>
</build>

Running Code Generation

I run the Maven goal to generate code:

Terminal window
mvn spring-crud-generator:generate

The plugin reads my YAML configuration and generates all layers in seconds.

Generated Code Structure

Here’s what gets generated:

src/main/java/
└── com/example/usermanagement/
├── entity/
│ └── User.java
├── dto/
│ ├── UserDTO.java
│ └── UserResponseDTO.java
├── mapper/
│ └── UserMapper.java
├── service/
│ ├── UserService.java
│ └── impl/
│ └── UserServiceImpl.java
├── controller/
│ └── UserController.java
└── repository/
└── UserRepository.java
src/main/resources/
└── db/migration/
└── V1__create_users_table.sql
Dockerfile
docker-compose.yml

Generated Entity Example

User.java
package com.example.usermanagement.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "users", uniqueConstraints = {
@UniqueConstraint(columnNames = "username"),
@UniqueConstraint(columnNames = "email")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
// Getters and setters
}

Generated Controller Example

UserController.java
package com.example.usermanagement.controller;
import com.example.usermanagement.dto.UserDTO;
import com.example.usermanagement.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Create a new user")
@ApiResponse(responseCode = "201", description = "User created successfully")
public UserDTO createUser(@RequestBody UserDTO userDTO) {
return userService.create(userDTO);
}
@GetMapping("/{id}")
@Operation(summary = "Get user by ID")
@ApiResponse(responseCode = "200", description = "User found")
@ApiResponse(responseCode = "404", description = "User not found")
public UserDTO getUser(@PathVariable Long id) {
return userService.findById(id);
}
@GetMapping
@Operation(summary = "List all users with pagination")
public Page<UserDTO> listUsers(Pageable pageable) {
return userService.findAll(pageable);
}
@PutMapping("/{id}")
@Operation(summary = "Update an existing user")
public UserDTO updateUser(@PathVariable Long id, @RequestBody UserDTO userDTO) {
return userService.update(id, userDTO);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete a user")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
}

Generated Flyway Migration

V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);

Generated Dockerfile

Dockerfile
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Why This Approach Works

I think the key benefits are:

Consistency: All entities follow the same architecture pattern. When I add a new entity, I don’t need to remember which annotations go on the DTO or how to structure the service layer.

Speed: I generate full CRUD for 10 entities in under a minute. When I need to iterate on the schema, I modify the YAML file and regenerate.

Best practices: The generated code follows Spring Boot conventions—proper separation of concerns, validation annotations, pagination support, and OpenAPI documentation.

Database flexibility: I can switch from PostgreSQL to MySQL by changing one line in the YAML configuration. The plugin adjusts the generated SQL migrations accordingly.

Version control: Flyway migrations are generated automatically, so database schema changes are tracked from the start.

When to Use Code Generation

I use code generation when:

  • Building CRUD-heavy applications (admin panels, internal tools)
  • Working with well-defined domain models
  • Need to prototype quickly
  • Want consistent architecture across entities

I stick to manual coding when:

  • Business logic doesn’t fit standard CRUD patterns
  • I need highly customized endpoints or workflows
  • Performance optimization requires specific query patterns
  • I’m learning Spring Boot fundamentals

Other Maven Plugins for Code Generation

There are other options, but they serve different purposes:

Spring Boot Maven Plugin: Official plugin for packaging and running Spring Boot apps. Not a code generator—just a build tool.

jOOQ Code Generator: Generates Java classes from database schema. Good for database-first projects, but only generates DAOs, not services or controllers.

MapStruct Generator: Annotation processor that generates mapper interfaces. I use it for entity-DTO mapping, but it doesn’t generate full CRUD applications.

OpenAPI Generator: Generates Spring controllers from OpenAPI specs. Works for API-first development, but doesn’t generate entities or database layers.

Spring CRUD Generator stands out because it generates the full stack from configuration—entities, DTOs, mappers, services, controllers, migrations, and Docker resources.

Summary

In this post, I showed how to use Spring CRUD Generator to automate Spring Boot CRUD code generation. The key point is that defining entities in YAML configuration and generating all layers consistently saves time and reduces boilerplate errors.

Instead of writing 300+ lines of repetitive code per entity, I define the data model once and let the Maven plugin generate entities, DTOs, services, controllers, migrations, API documentation, and Docker configuration in seconds.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments