Backend Development 16 min read

How to Build Your Own Spring Boot Scaffolding Tool with FreeMarker

This article explains why and when to create custom scaffolding for large-scale Java projects, compares popular options like Spring Initializr and Aliyun, and provides a step‑by‑step guide—including project structure, FreeMarker templates, and code generation classes—to build your own Spring Boot scaffolding tool.

macrozheng
macrozheng
macrozheng
How to Build Your Own Spring Boot Scaffolding Tool with FreeMarker

1. Introduction

Why build our own wheels? The core purpose of building a wheel is to encapsulate and reuse common solutions. While many mature wheels (services, frameworks, components, tools) exist, large enterprises often need custom wheels to support the scale of their systems. Off‑the‑shelf wheels like SpringBoot or Netty may not fit special scenarios, so a tailored scaffolding solution is required.

Scaffolding helps unify configuration, components, services, and tests under a standard, reducing repetitive copy‑paste work and improving consistency across projects.

2. What Is Scaffolding?

Scaffolding is a meta‑programming method of building database‑backed software applications. It is a technique supported by some model‑view‑controller frameworks, in which the programmer writes a specification describing how the application database may be used. The compiler uses this specification to generate code that the application can use to create, read, update and delete database entries, effectively treating the template as a "scaffold" on which to build a more powerful application.

In practice, scaffolding simplifies repetitive tasks by generating boilerplate code from a specification, allowing developers to focus on business logic.

3. Who Provides Scaffolding?

3.1 Spring Initializr

Rating: ⭐⭐⭐⭐

Link: https://start.spring.io

Source: https://github.com/spring-io/start.spring.io

Description: Spring Initializr is a web application that can generate a basic Spring Boot project structure via the web UI, Spring Tool Suite, IntelliJ IDEA, etc. Its source can also be deployed locally.

3.2 Aliyun Java Initializr

Rating: ⭐⭐⭐⭐

Link: https://start.aliyun.io

Description: Aliyun Java Initializr is a code‑framework generator similar to Spring Initializr. It offers one‑click project generation, a complete toolchain, and a free IDEA plugin, making it convenient for domestic users.

Both tools can quickly generate a standard project structure, but they may not include company‑specific components such as custom RPC, database routing, or message queues.

4. Building a Custom Scaffolding Tool

Why create our own scaffolding when existing ones exist? Because we need to integrate internal components and enforce company‑wide standards.

4.1 Project Structure

<code>EasyRiggerInitializr
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.initializr.rigger
    │   │       ├── application
    │   │       │   └── IProjectGenerator.java
    │   │       ├── domain
    │   │       │   ├── model
    │   │       │   │   ├── ApplicationInfo.java
    │   │       │   │   └── ProjectInfo.java
    │   │       │   └── service
    │   │       │       ├── module
    │   │       │       │   ├── impl
    │   │       │       │   │   ├── GenerationApplication.java
    │   │       │       │   │   ├── GenerationIgnore.java
    │   │       │       │   │   ├── GenerationPackageInfo.java
    │   │       │       │   │   ├── GenerationPom.java
    │   │       │       │   │   ├── GenerationTest.java
    │   │       │       │   │   └── GenerationYml.java
    │   │       │       │   └── BaseModule.java
    │   │       │       └── ProjectGeneratorImpl.java
    │   │       └── RiggerApplication.java
    │   └── resources
    │       ├── generator
    │       │   ├── application.ftl
    │       │   ├── ignore.ftl
    │       │   ├── package-info.ftl
    │       │   ├── pom.ftl
    │       │   ├── test.ftl
    │       │   └── yml.ftl
    │       └── application.yml
    └── test
        └── java
            └── cn.bugstack.initializr.rigger.test
                └── ApiTest.java</code>

4.2 Application Layer Interface

<code>public interface IProjectGenerator {
    void generator(ProjectInfo projectInfo) throws Exception;
}</code>

This thin application‑layer interface allows external callers to trigger project generation, while the domain layer provides the concrete implementation.

4.3 FreeMarker Templates

application.ftl

<code>package ${packageName};

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ${className} {
    public static void main(String[] args) {
        SpringApplication.run(${className}.class, args);
    }
}
</code>

pom.ftl

<code><?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>${groupId}</groupId>
    <artifactId>${artifactId}</artifactId>
    <version>${version}</version>
    <name>${name}</name>
    <description>${description}</description>
</project>
</code>

yml.ftl

<code>server:
  port: 8081
</code>

4.4 Generation Logic

GenerationApplication.java

<code>@Service
public class GenerationApplication extends BaseModule {
    private Logger logger = LoggerFactory.getLogger(GenerationApplication.class);

    public void doGeneration(ProjectInfo projectInfo, String projectsRoot, String lastPackageName, StringBuffer applicationJavaName) throws Exception {
        ApplicationInfo applicationInfo = new ApplicationInfo(
                projectInfo.getGroupId() + "." + lastPackageName,
                applicationJavaName.toString()
        );
        String packagePath = applicationInfo.getPackageName().replace(".", "/") + "/";
        File file = new File(projectsRoot + projectInfo.getArtifactId() + "/src/main/java/" + packagePath,
                applicationInfo.getClassName() + ".java");
        // write file
        super.writeFile(file, "application.ftl", applicationInfo);
        logger.info("Created Application.java {}", file.getPath());
    }
}
</code>

ProjectGeneratorImpl.java

<code>@Service
public class ProjectGeneratorImpl implements IProjectGenerator {
    private Logger logger = LoggerFactory.getLogger(ProjectGeneratorImpl.class);

    @Resource private GenerationApplication generationApplication;
    @Resource private GenerationYml generationYml;
    @Resource private GenerationPom generationPom;
    @Resource private GenerationTest generationTest;
    @Resource private GenerationIgnore generationIgnore;
    @Resource private GenerationPackageInfo generationPackageInfo;

    @Override
    public void generator(ProjectInfo projectInfo) throws Exception {
        URL resource = this.getClass().getResource("/");
        String projectsRoot = resource.getFile() + "/projects/";
        String lastPackageName = projectInfo.getArtifactId().replaceAll("-", "").toLowerCase();
        String[] split = projectInfo.getArtifactId().split("-");
        StringBuffer applicationJavaName = new StringBuffer();
        Arrays.asList(split).forEach(s -> {
            applicationJavaName.append(s.substring(0,1).toUpperCase() + s.substring(1));
        });
        applicationJavaName.append("Application");
        // 1. Application.java
        generationApplication.doGeneration(projectInfo, projectsRoot, lastPackageName, applicationJavaName);
        // 2. application.yml
        generationYml.doGeneration(projectInfo, projectsRoot);
        // 3. pom.xml
        generationPom.doGeneration(projectInfo, projectsRoot);
        // 4. ApiTest.java
        generationTest.doGeneration(projectInfo, projectsRoot, lastPackageName, applicationJavaName);
        // 5. .gitignore
        generationIgnore.doGeneration(projectInfo, projectsRoot);
        // 6. DDD layer description files
        generationPackageInfo.doGeneration(projectInfo, projectsRoot, lastPackageName, applicationJavaName);
    }
}
</code>

The implementation creates the main entry class, configuration files, Maven POM, test class, .gitignore, and DDD layer descriptors.

4.5 Unit Test

<code>@Test
public void test_IProjectGenerator() throws Exception {
    ProjectInfo projectInfo = new ProjectInfo(
            "cn.bugstack.demo",
            "web-test",
            "1.0.0-SNAPSHOT",
            "web-test",
            "Demo project for Spring Boot"
    );
    iProjectGenerator.generator(projectInfo);
}
</code>

The test generates the project under the

test-classes

directory, which can then be opened in an IDE like IntelliJ IDEA.

5. Source Code Download

Source code: https://github.com/fuzhengwei/EasyRiggerInitializr

Project description: A Spring Boot scaffolding tool that simplifies project creation. It is lightweight and suitable for beginners, with future plans to integrate RPC, MQ, service registry, gateway, and other components.

6. Summary

From a company perspective, avoiding duplicate wheels reduces cost, but for individuals, building wheels deepens technical knowledge and can improve career prospects.

Understanding the fundamentals of component extraction and service decomposition is essential for long‑term growth.

Even if the organization does not need a custom wheel, you can still create one for personal learning and contribute it to the open‑source community.

code generationBackend DevelopmentSpring Bootscaffoldingfreemarker
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.