Spring Boot Application Structure

3 minute read

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

  1. Controller → Service → Repository — the standard layered pattern.
  2. Package by feature for larger projects — scales better than package by layer.
  3. @ComponentScan starts from the main application class’s package.
  4. Spring manages bean lifecycle — use @PostConstruct / @PreDestroy for init/cleanup.
  5. Never expose JPA entities directly — use DTOs.

What’s Next?

Next: Auto-configuration — how Spring Boot magically configures everything based on your classpath.

Updated: