Spring Initializr is one of the best tools in the Java ecosystem.
It solves the first problem every Spring Boot developer has:
"How do I create a working Spring Boot application quickly?"
You select dependencies, choose your Java version, generate the ZIP, and you have a running application in seconds.
But after that first commit, every team faces the same question:
"How should we actually structure this application?"
Because Spring Initializr gives you a starting point — not a production architecture.
Let's look at how many real-world Spring Boot projects evolve beyond the initial scaffold.
The Default Spring Initializr Structure
A freshly generated project usually looks like this:
src/main/java/com/example/app
├── Application.java
And technically, that is enough.
You can start adding:
controllers
services
repositories
entities
configuration
security
But Spring does not enforce where those things go.
That flexibility is powerful.
It also means every new project requires architecture decisions.
1. Separate Controllers From Business Logic
A common early mistake is putting too much logic inside controllers.
Example:
@RestController
@RequestMapping("/users")
class UserController {
@PostMapping
public User createUser(
@RequestBody User user
) {
validateUser(user);
user.setCreatedAt(
Instant.now()
);
sendWelcomeEmail(user);
return userRepository.save(user);
}
}
It works.
But controllers quickly become responsible for:
- request handling
- validation
- business rules
- persistence logic
Instead, keep controllers thin:
Controller
|
v
Service
|
v
Repository
Example:
@RestController
class UserController {
private final UserService userService;
@PostMapping("/users")
UserResponse create(
@RequestBody CreateUserRequest request
) {
return userService.create(request);
}
}
The controller handles HTTP.
The service owns business logic.
2. Add DTOs Instead of Exposing Entities
A common shortcut:
@GetMapping("/users/{id}")
public User getUser() {
return userRepository.findById(id);
}
The problem?
Your database model becomes your API contract.
Changing a column, renaming a field, or adding internal data can accidentally change what your API exposes.
Later changes become risky.
Instead:
Entity
|
Mapper
|
DTO
|
API Response
Example:
public record UserResponse(
Long id,
String email
) {}
Benefits:
- safer APIs
- easier versioning
- prevents leaking internal fields
- separates persistence from contracts
3. Create a Dedicated Exception Layer
Without centralized exception handling:
try {
}
catch(Exception e){
}
appears everywhere.
Spring provides a cleaner pattern:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
ResponseEntity<?> handle(
Exception ex
) {
return ResponseEntity
.status(500)
.body(
ex.getMessage()
);
}
}
Note: A real implementation usually handles specific exception types with appropriate status codes — 404 for not found, 400 for validation errors, 403 for access denied.
Now errors are handled consistently.
Your controllers stay focused.
4. Keep Configuration Isolated
Production applications eventually need:
- security configuration
- CORS rules
- authentication filters
- database configuration
- external service clients
Avoid scattering configuration everywhere.
Use:
config/
├── SecurityConfig.java
├── CorsConfig.java
├── AppConfig.java
It keeps infrastructure concerns separate from business code.
5. Validate Requests at the Boundary
Do not let invalid data travel deep into your application.
Example:
public record CreateUserRequest(
@NotBlank
String name,
@Email
String email
) {}
Validation keeps services focused on business rules instead of checking basic request correctness.
6. Organize Security Separately
Authentication grows quickly.
A basic project may start with:
SecurityConfig.java
Then later needs:
security/
├── JwtService.java
├── JwtAuthenticationFilter.java
├── OAuth2SuccessHandler.java
├── RefreshTokenService.java
Keeping security isolated makes the project easier to maintain.
7. Add Environment Separation Early
Many projects start with:
application.yml
Eventually they need:
application.yml
application-dev.yml
application-prod.yml
Why?
Local:
localhost database
debug logging
local secrets
Production:
environment variables
secure configs
optimized logging
Separating environments early prevents painful migrations later.
8. Choose Layer-Based vs Feature-Based Structure
Some teams prefer organizing by feature instead of layer:
src/main/java/com/company/app
├── user/
│ ├── UserController.java
│ ├── UserService.java
│ ├── UserRepository.java
│ └── UserResponse.java
├── order/
│ ├── OrderController.java
│ └── OrderService.java
Feature-based structure scales better for larger applications where each domain grows independently.
Layered structure is simpler for smaller applications and easier for teams new to the codebase.
9. A More Production-Friendly Structure
A common structure:
src/main/java/com/company/app
├── controller
├── service
├── repository
├── entity
├── dto
├── mapper
├── exception
├── config
├── security
└── Application.java
This is not the only correct structure.
But it gives teams a predictable foundation.
Spring Boot Structure Should Grow With Your Application
Not every project needs:
- Kubernetes
- microservices
- complex architecture
- dozens of modules
Starting simple is good.
The goal is not adding folders.
The goal is separating responsibilities.
A good Spring Boot foundation should make future changes easier, not harder.
Automating the Repeated Setup
Many teams eventually create internal templates or starter repositories to standardize this setup.
That repeated setup is what SpringGen is designed to solve.
SpringGen generates Spring Boot project foundations with standard structure, authentication, database configuration, Docker, CI/CD, and deployment files included — so development starts at business logic, not boilerplate.
What structure do you usually prefer for Spring Boot projects — layered, feature-based, or something else?
Top comments (1)
One thing I've started appreciating lately is a hybrid approach.
Feature-based organization for business domains (
auth,users,files) and then shared packages for cross-cutting concerns (config,security,exception).Pure layer-based structures become difficult to navigate as the project grows, while pure feature-based structures can lead to duplicated infrastructure code.
The bigger lesson is the same one you highlighted: project structure should make future changes easier, not just look organized on day one.