DEV Community

Cover image for How to Structure Spring Boot Projects Beyond Spring Initializr
Yadrs
Yadrs

Posted on

How to Structure Spring Boot Projects Beyond Spring Initializr

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
Enter fullscreen mode Exit fullscreen mode

And technically, that is enough.

You can start adding:

controllers
services
repositories
entities
configuration
security
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

It works.

But controllers quickly become responsible for:

  • request handling
  • validation
  • business rules
  • persistence logic

Instead, keep controllers thin:

Controller
     |
     v
Service
     |
     v
Repository
Enter fullscreen mode Exit fullscreen mode

Example:

@RestController
class UserController {

    private final UserService userService;

    @PostMapping("/users")
    UserResponse create(
        @RequestBody CreateUserRequest request
    ) {
        return userService.create(request);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Example:

public record UserResponse(
    Long id,
    String email
) {}
Enter fullscreen mode Exit fullscreen mode

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){

}
Enter fullscreen mode Exit fullscreen mode

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()
            );
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

) {}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then later needs:

security/

├── JwtService.java
├── JwtAuthenticationFilter.java
├── OAuth2SuccessHandler.java
├── RefreshTokenService.java
Enter fullscreen mode Exit fullscreen mode

Keeping security isolated makes the project easier to maintain.


7. Add Environment Separation Early

Many projects start with:

application.yml
Enter fullscreen mode Exit fullscreen mode

Eventually they need:

application.yml

application-dev.yml

application-prod.yml
Enter fullscreen mode Exit fullscreen mode

Why?

Local:

localhost database
debug logging
local secrets
Enter fullscreen mode Exit fullscreen mode

Production:

environment variables
secure configs
optimized logging
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

https://app.springgen.dev


What structure do you usually prefer for Spring Boot projects — layered, feature-based, or something else?

Top comments (1)

Collapse
 
buildbasekit profile image
buildbasekit

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.