How to Build a Reusable Spring Boot Backend Template for Fast Project Kickoff
This article walks through the author's experience of creating a comprehensive Spring Boot backend template—including project scaffolding, one‑click scripts, business‑oriented package layout, automated test classification, logging, exception handling, distributed locks, Swagger API docs, Flyway migrations, multi‑environment builds, CORS setup, and static code checks—so new developers can start coding with minimal friction.
Project Template Overview
A Spring Boot starter project is provided to standardise scaffolding, testing, logging, error handling, background jobs, API documentation, database migration, environment profiles, CORS and static analysis.
git clone https://github.com/e-commerce-sample/order-backend git checkout a443dace
One‑Click Local Build
Three helper scripts simplify the developer workflow: idea.sh – generates an IntelliJ project and opens it. run.sh – starts the application, launches a local MySQL container and opens port 5005 for debugging. local-build.sh – runs the Gradle build; only successful builds may be committed.
Typical workflow:
Pull the code.
Run idea.sh to open the IDE.
Write business code and tests.
Optionally run run.sh for manual testing.
Run local-build.sh to verify the build.
Pull again, ensure the build succeeds, then commit.
#!/usr/bin/env bash
./gradlew clean bootRunDirectory Structure
order-backend/
├── gradle/ # Gradle configuration (e.g., checkstyle)</n├── src/ # Java source code
├── idea.sh # IntelliJ project generator
├── run.sh # Local run script
└── local-build.sh # Pre‑commit build scriptBusiness‑Oriented Package Layout
Code is grouped by domain aggregates rather than technical layers.
order/
│ ├── OrderApplicationService.java
│ ├── OrderController.java
│ ├── OrderNotFoundException.java
│ ├── OrderRepository.java
│ ├── OrderService.java
│ └── model/
│ ├── Order.java
│ ├── OrderFactory.java
│ ├── OrderId.java
│ ├── OrderItem.java
│ └── OrderStatus.java
product/
│ ├── Product.java
│ ├── ProductApplicationService.java
│ ├── ProductController.java
│ ├── ProductId.java
│ └── ProductRepository.java
common/
├── configuration/
├── exception/
├── logging/
└── utils/Automated Test Classification
Three test types are defined and isolated via Gradle sourceSets:
sourceSets {
componentTest {
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += sourceSets.main.output + sourceSets.test.output
}
apiTest {
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += sourceSets.main.output + sourceSets.test.output
}
}Corresponding directories:
Unit tests: src/test/java Component tests: src/componentTest/java API tests: src/apiTest/java Component and API tests require a MySQL container; the Gradle docker‑compose plugin starts it automatically:
apply plugin: 'docker-compose'
dockerCompose {
useComposeFiles = ['docker/mysql/docker-compose.yml']
}
bootRun.dependsOn composeUp
componentTest.dependsOn composeUp
apiTest.dependsOn composeUpLog Handling
Two enhancements are added:
Request‑ID injection via Logback MDC for traceability.
Centralised log aggregation using a Redis appender that forwards logs to Logstash.
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String headerRequestId = request.getHeader(HEADER_X_REQUEST_ID);
MDC.put(REQUEST_ID, isNullOrEmpty(headerRequestId) ? newUuid() : headerRequestId);
try {
filterChain.doFilter(request, response);
} finally {
clearMdc();
}
} <appender name="REDIS" class="com.cwbase.logback.RedisAppender">
<tags>ecommerce-order-backend-${ACTIVE_PROFILE}</tags>
<host>elk.yourdomain.com</host>
<port>6379</port>
<password>whatever</password>
<key>ecommerce-ordder-log</key>
<mdc>true</mdc>
<type>redis</type>
</appender>Exception Handling
A hierarchical model uses a base AppException that carries an ErrorCode enum, HTTP status and a data map.
public abstract class AppException extends RuntimeException {
private final ErrorCode code;
private final Map<String, Object> data = newHashMap();
}Specific exception example:
public class OrderNotFoundException extends AppException {
public OrderNotFoundException(OrderId orderId) {
super(ErrorCode.ORDER_NOT_FOUND,
ImmutableMap.of("orderId", orderId.toString()));
}
}Uniform error response:
{
"requestId": "d008ef46bb4f4cf19c9081ad50df33bd",
"error": {
"code": "ORDER_NOT_FOUND",
"status": 404,
"message": "没有找到订单",
"path": "/order",
"timestamp": 1555031270087,
"data": {"orderId": "123456789"}
}
}Background Tasks and Distributed Lock
Spring async and scheduling are enabled with a custom thread pool. ShedLock provides a lightweight JDBC‑based distributed lock.
@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(newScheduledThreadPool(10));
}
@Bean(destroyMethod = "shutdown")
@Primary
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(10);
executor.setTaskDecorator(new LogbackMdcTaskDecorator());
executor.initialize();
return executor;
}
} @Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class DistributedLockConfiguration {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
@Bean
public DistributedLockExecutor distributedLockExecutor(LockProvider lockProvider) {
return new DistributedLockExecutor(lockProvider);
}
} public class DistributedLockExecutor {
private final LockProvider lockProvider;
public DistributedLockExecutor(LockProvider lockProvider) {
this.lockProvider = lockProvider;
}
public <T> T executeWithLock(Supplier<T> supplier, LockConfiguration configuration) {
Optional<SimpleLock> lock = lockProvider.lock(configuration);
if (!lock.isPresent()) {
throw new LockAlreadyOccupiedException(configuration.getName());
}
try {
return supplier.get();
} finally {
lock.get().unlock();
}
}
}Usage example:
public String doBusiness() {
return distributedLockExecutor.executeWithLock(
() -> "Hello World.",
new LockConfiguration("key", Instant.now().plusSeconds(60)));
}Unified Code Style
Conventions enforced via Checkstyle and team guidelines include:
Request DTO suffix Command, response DTO suffix Representation.
Consistent three‑layer or DDD tactical architecture.
Standardised pagination classes.
Shared test‑base class and explicit test classification.
Static Code Checks
Gradle plugins used for continuous quality monitoring:
Checkstyle – code format.
SpotBugs – static analysis.
OWASP Dependency‑Check – dependency security.
Sonar – code quality dashboard.
Health Check Endpoint
A lightweight endpoint returns HTTP 200 with build metadata:
{
"requestId": "698c8d29add54e24a3d435e2c749ea00",
"buildNumber": "unknown",
"buildTime": "unknown",
"deployTime": "2019-04-11T13:05:46.901+08:00[Asia/Shanghai]",
"gitRevision": "unknown",
"gitBranch": "unknown",
"environment": "[local]"
}API Documentation
Swagger (Springfox) automatically generates up‑to‑date API docs.
@Configuration
@EnableSwagger2
@Profile({"local", "dev"})
public class SwaggerConfiguration {
@Bean
public Docket api() {
return new Docket(SWAGGER_2)
.select()
.apis(basePackage("com.ecommerce.order"))
.paths(any())
.build();
}
}Running the application and visiting http://localhost:8080/swagger-ui.html displays the interactive UI.
Database Migration
Flyway manages schema evolution. Migration scripts live under src/main/resources/db/migration and follow the V{version}__{description}.sql naming convention.
resources/
└── db/
└── migration/
├── V1__init.sql
└── V2__create_product_table.sqlScripts must not be edited after they have been applied because Flyway validates checksums.
Multi‑Environment Build
Six profiles are defined, each with its own configuration files: local – developer workstation. ci – continuous‑integration builds. dev – front‑end integration. qa – QA testing. uat – staging/acceptance. prod – production.
CORS Configuration
Global CORS mapping for Spring Boot:
@Configuration
public class CorsConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}If Spring Security is used, the CORS source must be applied before the security filter chain:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and();
// other security config …
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}Common Third‑Party Libraries
Guava
Apache Commons
Mockito
DBUnit
Rest Assured
Jackson 2
jjwt
Lombok
Feign
Tika
iText
Zxing
XStream
Java Web Project
Focused on Java backend technologies, trending internet tech, and the latest industry developments. The platform serves over 200,000 Java developers, inviting you to learn and exchange ideas together. Check the menu for Java learning resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
