Spring Boot Application Structure
A well-structured Spring Boot application is easy to navigate, test, and maintain. This chapter covers the standard layered architecture and package organization.
Controller-Service-Repository Pattern
Client (Browser/Postman)
│
▼
┌──────────────┐
│ Controller │ ← Handles HTTP requests/responses
│ @RestController│
└──────┬───────┘
│
▼
┌──────────────┐
│ Service │ ← Business logic
│ @Service │
└──────┬───────┘
│
▼
┌──────────────┐
│ Repository │ ← Database access
│ @Repository │
└──────┬───────┘
│
▼
[ Database ]
| Layer | Responsibility | Annotation |
|---|---|---|
| Controller | HTTP handling, request/response mapping | @RestController |
| Service | Business rules, validation, orchestration | @Service |
| Repository | Database queries, CRUD operations | @Repository |
| Model/Entity | Data structures, JPA entities | @Entity |
| DTO | Data transfer between layers | Plain class / record |
Package Organization
Option 1: Package by Layer (Simple Projects)
com.bookstore/
├── controller/
│ ├── BookController.java
│ └── AuthorController.java
├── service/
│ ├── BookService.java
│ └── AuthorService.java
├── repository/
│ ├── BookRepository.java
│ └── AuthorRepository.java
├── model/
│ ├── Book.java
│ └── Author.java
├── dto/
│ ├── BookRequest.java
│ └── BookResponse.java
├── exception/
│ └── ResourceNotFoundException.java
└── BookstoreApiApplication.java
Option 2: Package by Feature (Larger Projects — Recommended)
com.bookstore/
├── book/
│ ├── BookController.java
│ ├── BookService.java
│ ├── BookRepository.java
│ ├── Book.java
│ ├── BookRequest.java
│ └── BookResponse.java
├── author/
│ ├── AuthorController.java
│ ├── AuthorService.java
│ ├── AuthorRepository.java
│ └── Author.java
├── common/
│ ├── exception/
│ └── config/
└── BookstoreApiApplication.java
Package by feature scales better — each feature is self-contained and can become a separate module/microservice later.
The Application Context
The ApplicationContext is Spring’s IoC container — it manages all beans (objects):
@SpringBootApplication
public class BookstoreApiApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(BookstoreApiApplication.class, args);
// List all beans
String[] beanNames = context.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String name : beanNames) {
System.out.println(name);
}
}
}
Spring Boot auto-registers hundreds of beans — your controllers, services, repositories, and all auto-configured components.
Component Scanning
@SpringBootApplication triggers @ComponentScan starting from its package:
com.bookstore/ ← @SpringBootApplication is here
├── controller/BookController ← ✅ Found (sub-package)
├── service/BookService ← ✅ Found (sub-package)
└── BookstoreApiApplication ← The main class
com.other/ ← ❌ NOT scanned
└── SomeClass ← Won't be found
To scan additional packages:
@SpringBootApplication
@ComponentScan(basePackages = {"com.bookstore", "com.shared"})
public class BookstoreApiApplication { }
Bean Lifecycle
1. Spring scans for @Component, @Service, @Repository, @Controller
2. Instantiates each class
3. Injects dependencies (constructor injection)
4. Calls @PostConstruct methods
5. Bean is ready for use
...
6. Application shuts down
7. Calls @PreDestroy methods
8. Bean is destroyed
@Service
public class BookService {
@PostConstruct
public void init() {
System.out.println("BookService initialized");
// Load cache, validate config, etc.
}
@PreDestroy
public void cleanup() {
System.out.println("BookService shutting down");
// Close resources, flush buffers
}
}
Our Bookstore API Structure
We’ll build this throughout the Spring Boot module:
// Controller
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping
public List<BookResponse> getAllBooks() {
return bookService.findAll();
}
}
// Service
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public List<BookResponse> findAll() {
return bookRepository.findAll().stream()
.map(this::toResponse)
.toList();
}
}
// Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}
Common Mistakes
| Mistake | Fix |
|---|---|
| Main class in the wrong package | Must be in the root package for scanning to work |
| Service calling another service’s repository directly | Each layer should call only the layer below it |
| Business logic in controllers | Move logic to service classes |
| Exposing JPA entities in API responses | Use DTOs (Data Transfer Objects) |
Key Takeaways
- Controller → Service → Repository — the standard layered pattern.
- Package by feature for larger projects — scales better than package by layer.
@ComponentScanstarts from the main application class’s package.- Spring manages bean lifecycle — use
@PostConstruct/@PreDestroyfor init/cleanup. - Never expose JPA entities directly — use DTOs.
What’s Next?
Next: Auto-configuration — how Spring Boot magically configures everything based on your classpath.