{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreiet2uqhvwh2jjh55lktltokctzcxmrbydk6jtdsyq4wihzayiazzu",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mopydowiwug2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreidxv3ahxyho3rxlqayitnjsj7cdcx4em3ew46xu73xkbumxyjynru"
},
"mimeType": "image/webp",
"size": 154498
},
"path": "/yadrs/how-to-structure-spring-boot-projects-beyond-spring-initializr-fac",
"publishedAt": "2026-06-20T13:03:47.000Z",
"site": "https://dev.to",
"tags": [
"springboot",
"spring",
"java",
"backend",
"https://app.springgen.dev",
"@RestController",
"@RequestMapping",
"@PostMapping",
"@RequestBody",
"@GetMapping",
"@RestControllerAdvice",
"@ExceptionHandler",
"@NotBlank",
"@Email"
],
"textContent": "Spring Initializr is one of the best tools in the Java ecosystem.\n\nIt solves the first problem every Spring Boot developer has:\n\n> \"How do I create a working Spring Boot application quickly?\"\n\nYou select dependencies, choose your Java version, generate the ZIP, and you have a running application in seconds.\n\nBut after that first commit, every team faces the same question:\n\n> \"How should we actually structure this application?\"\n\nBecause Spring Initializr gives you a starting point — not a production architecture.\n\nLet's look at how many real-world Spring Boot projects evolve beyond the initial scaffold.\n\n## The Default Spring Initializr Structure\n\nA freshly generated project usually looks like this:\n\n\n\n src/main/java/com/example/app\n\n ├── Application.java\n\n\nAnd technically, that is enough.\n\nYou can start adding:\n\n\n\n controllers\n services\n repositories\n entities\n configuration\n security\n\n\nBut Spring does not enforce where those things go.\n\nThat flexibility is powerful.\n\nIt also means every new project requires architecture decisions.\n\n### 1. Separate Controllers From Business Logic\n\nA common early mistake is putting too much logic inside controllers.\n\nExample:\n\n\n\n @RestController\n @RequestMapping(\"/users\")\n class UserController {\n\n @PostMapping\n public User createUser(\n @RequestBody User user\n ) {\n\n validateUser(user);\n\n user.setCreatedAt(\n Instant.now()\n );\n\n sendWelcomeEmail(user);\n\n return userRepository.save(user);\n }\n }\n\n\nIt works.\n\nBut controllers quickly become responsible for:\n\n * request handling\n * validation\n * business rules\n * persistence logic\n\n\n\nInstead, keep controllers thin:\n\n\n\n Controller\n |\n v\n Service\n |\n v\n Repository\n\n\nExample:\n\n\n\n @RestController\n class UserController {\n\n private final UserService userService;\n\n @PostMapping(\"/users\")\n UserResponse create(\n @RequestBody CreateUserRequest request\n ) {\n return userService.create(request);\n }\n }\n\n\nThe controller handles HTTP.\n\nThe service owns business logic.\n\n### 2. Add DTOs Instead of Exposing Entities\n\nA common shortcut:\n\n\n\n @GetMapping(\"/users/{id}\")\n public User getUser() {\n return userRepository.findById(id);\n }\n\n\nThe problem?\n\nYour database model becomes your API contract.\n\nChanging a column, renaming a field, or adding internal data can accidentally change what your API exposes.\n\nLater changes become risky.\n\nInstead:\n\n\n\n Entity\n |\n Mapper\n |\n DTO\n |\n API Response\n\n\nExample:\n\n\n\n public record UserResponse(\n Long id,\n String email\n ) {}\n\n\nBenefits:\n\n * safer APIs\n * easier versioning\n * prevents leaking internal fields\n * separates persistence from contracts\n\n\n\n### 3. Create a Dedicated Exception Layer\n\nWithout centralized exception handling:\n\n\n\n try {\n\n }\n catch(Exception e){\n\n }\n\n\nappears everywhere.\n\nSpring provides a cleaner pattern:\n\n\n\n @RestControllerAdvice\n public class GlobalExceptionHandler {\n\n @ExceptionHandler(Exception.class)\n ResponseEntity<?> handle(\n Exception ex\n ) {\n return ResponseEntity\n .status(500)\n .body(\n ex.getMessage()\n );\n }\n }\n\n\nNote: A real implementation usually handles specific exception types with appropriate status codes — 404 for not found, 400 for validation errors, 403 for access denied.\n\nNow errors are handled consistently.\n\nYour controllers stay focused.\n\n### 4. Keep Configuration Isolated\n\nProduction applications eventually need:\n\n * security configuration\n * CORS rules\n * authentication filters\n * database configuration\n * external service clients\n\n\n\nAvoid scattering configuration everywhere.\n\nUse:\n\n\n\n config/\n\n ├── SecurityConfig.java\n ├── CorsConfig.java\n ├── AppConfig.java\n\n\nIt keeps infrastructure concerns separate from business code.\n\n### 5. Validate Requests at the Boundary\n\nDo not let invalid data travel deep into your application.\n\nExample:\n\n\n\n public record CreateUserRequest(\n\n @NotBlank\n String name,\n\n @Email\n String email\n\n ) {}\n\n\nValidation keeps services focused on business rules instead of checking basic request correctness.\n\n### 6. Organize Security Separately\n\nAuthentication grows quickly.\n\nA basic project may start with:\n\n\n\n SecurityConfig.java\n\n\nThen later needs:\n\n\n\n security/\n\n ├── JwtService.java\n ├── JwtAuthenticationFilter.java\n ├── OAuth2SuccessHandler.java\n ├── RefreshTokenService.java\n\n\nKeeping security isolated makes the project easier to maintain.\n\n### 7. Add Environment Separation Early\n\nMany projects start with:\n\n\n\n application.yml\n\n\nEventually they need:\n\n\n\n application.yml\n\n application-dev.yml\n\n application-prod.yml\n\n\nWhy?\n\nLocal:\n\n\n\n localhost database\n debug logging\n local secrets\n\n\nProduction:\n\n\n\n environment variables\n secure configs\n optimized logging\n\n\nSeparating environments early prevents painful migrations later.\n\n### 8. Choose Layer-Based vs Feature-Based Structure\n\nSome teams prefer organizing by feature instead of layer:\n\n\n\n src/main/java/com/company/app\n\n ├── user/\n │ ├── UserController.java\n │ ├── UserService.java\n │ ├── UserRepository.java\n │ └── UserResponse.java\n ├── order/\n │ ├── OrderController.java\n │ └── OrderService.java\n\n\nFeature-based structure scales better for larger applications where each domain grows independently.\n\nLayered structure is simpler for smaller applications and easier for teams new to the codebase.\n\n### 9. A More Production-Friendly Structure\n\nA common structure:\n\n\n\n src/main/java/com/company/app\n\n ├── controller\n ├── service\n ├── repository\n ├── entity\n ├── dto\n ├── mapper\n ├── exception\n ├── config\n ├── security\n └── Application.java\n\n\nThis is not the only correct structure.\n\nBut it gives teams a predictable foundation.\n\n## Spring Boot Structure Should Grow With Your Application\n\nNot every project needs:\n\n * Kubernetes\n * microservices\n * complex architecture\n * dozens of modules\n\n\n\nStarting simple is good.\n\nThe goal is not adding folders.\n\nThe goal is separating responsibilities.\n\nA good Spring Boot foundation should make future changes easier, not harder.\n\n## Automating the Repeated Setup\n\nMany teams eventually create internal templates or starter repositories to standardize this setup.\n\nThat repeated setup is what SpringGen is designed to solve.\n\nSpringGen 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.\n\n→ https://app.springgen.dev\n\nWhat structure do you usually prefer for Spring Boot projects — layered, feature-based, or something else?",
"title": "How to Structure Spring Boot Projects Beyond Spring Initializr"
}