V1
Back to handbooks index
Developer Handbook
Beginner → Intermediate Spring Boot 3.x
Developer Field Handbook

Spring Bootfrom first bean to production

// Convention over configuration. Opinionated. Production-ready.

A practical guide for developers learning Spring Boot — from wiring your first REST endpoint to writing tests, managing databases with JPA, and shipping a containerized service. No XML required.

Spring Boot 3.x Java 17+ REST APIs Spring Data JPA Spring Security
01

What is Spring Boot?

// FOUNDATION AND PHILOSOPHY

Spring Boot is an opinionated framework built on top of the Spring Framework. It auto-configures your application based on the dependencies on your classpath, embeds a web server (Tomcat by default), and gets you from zero to running in minutes — without XML, without boilerplate application server config, without manual wiring.

Auto-Configuration

Spring Boot detects what's on your classpath and configures beans automatically. Add spring-boot-starter-web and Tomcat is configured. Add a JPA dependency and a DataSource is wired. You override what you need, ignore the rest.

Starters

Starters are curated dependency bundles. Instead of hunting compatible versions of 8 libraries, you pull one starter. spring-boot-starter-data-jpa brings in Hibernate, Spring Data, and JDBC driver support — pre-wired and version-matched.

Embedded Server

Your application is a standalone JAR — the server lives inside it. No WAR deployment, no separate Tomcat install. Run with java -jar app.jar. Switch to Jetty or Undertow by excluding Tomcat and adding a different starter.

✕ Traditional Spring
  • Manual XML or Java config for every bean
  • Manage library version compatibility yourself
  • Deploy WAR to external app server
  • Configure DataSource, Transaction Manager, MVC manually
  • Hundreds of lines of boilerplate before business logic
✓ Spring Boot
  • Auto-configured from classpath and properties
  • Starters manage version-compatible dependency trees
  • Embedded server — run as plain JAR
  • DataSource, Hibernate, MVC configured automatically
  • Focus on business logic from line one
02

Project Setup

// INITIALIZR AND DEPENDENCIES

The fastest way to start is start.spring.io — Spring Initializr generates a ready-to-run project. Select your build tool, Java version, and starters. Below is a typical pom.xml for a REST API with a database.

XML — pom.xml (Maven)
<!-- Parent handles all Spring Boot version management --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> </parent> <dependencies> <!-- Web: REST controllers, embedded Tomcat, Jackson JSON --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Data: Spring Data JPA + Hibernate --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Validation: @Valid, @NotNull, @Size --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- DB driver (H2 for dev, swap to Postgres in prod) --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- Testing: JUnit 5, Mockito, MockMvc --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
Java — Main Application Class
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan public class BookstoreApplication { public static void main(String[] args) { SpringApplication.run(BookstoreApplication.class, args); } // That's it. Spring Boot wires everything else from the classpath. }
Dev Tools: Add spring-boot-devtools as a dependency during development. It enables automatic restarts when classpath files change — no manual restart loop. It's automatically excluded from production builds.
03

Project Structure

// CONVENTIONAL PACKAGE LAYOUT

Spring Boot doesn't enforce a folder structure, but the layered architecture below is the community standard. Each layer has a clear responsibility. Keeping them separate makes testing, maintenance, and onboarding dramatically easier.

Directory Layout
src/ ├── main/ │ ├── java/com/example/bookstore/ │ │ ├── BookstoreApplication.java // entry point │ │ │ │ │ ├── controller/ // @RestController — HTTP layer │ │ │ └── BookController.java │ │ │ │ │ ├── service/ // @Service — business logic │ │ │ └── BookService.java │ │ │ │ │ ├── repository/ // @Repository — data access │ │ │ └── BookRepository.java │ │ │ │ │ ├── model/ // @Entity — JPA domain objects │ │ │ └── Book.java │ │ │ │ │ ├── dto/ // Data Transfer Objects (API shapes) │ │ │ ├── BookRequest.java │ │ │ └── BookResponse.java │ │ │ │ │ └── exception/ // Custom exceptions + global handler │ │ ├── BookNotFoundException.java │ │ └── GlobalExceptionHandler.java │ │ │ └── resources/ │ ├── application.yml // config (db, port, logging) │ ├── application-dev.yml // profile-specific overrides │ └── db/migration/ // Flyway migration scripts │ └── V1__init.sql │ └── test/ └── java/com/example/bookstore/ ├── controller/ BookControllerTest.java └── service/ BookServiceTest.java
HTTP IN
Controller
Map routes, parse input, return responses
BUSINESS
Service
Orchestrate logic, transactions, rules
DATA
Repository
Query & persist via JPA
STORAGE
Database
PostgreSQL, MySQL, H2…
04

IoC & Dependency Injection

// THE HEART OF SPRING

Inversion of Control (IoC) means Spring manages object creation and wiring — not your code. You declare what you need via Dependency Injection and Spring provides it. This decouples components, makes testing trivial, and keeps classes focused on one responsibility.

Constructor Injection Recommended

Inject dependencies via the constructor. Makes dependencies explicit, supports immutability (final fields), and makes the class easy to test without a Spring context — just pass mocks to the constructor.

Field Injection Avoid

Using @Autowired directly on a field. Hides dependencies, makes testing harder (requires reflection or a Spring context), and breaks with final. Lombok's @RequiredArgsConstructor makes constructor injection painless.

Java — Constructor Injection (correct way)
@Service public class BookService { private final BookRepository bookRepository; private final NotificationService notificationService; // Spring sees ONE constructor — auto-wires without @Autowired public BookService(BookRepository bookRepository, NotificationService notificationService) { this.bookRepository = bookRepository; this.notificationService = notificationService; } // With Lombok — exactly equivalent, less noise: // @RequiredArgsConstructor on the class generates this constructor }
05

Beans & Core Annotations

// WHAT SPRING MANAGES AND HOW

A bean is any object managed by the Spring ApplicationContext. You declare beans by annotating classes. Spring instantiates them, injects their dependencies, and manages their lifecycle.

AnnotationUsed OnPurpose
@SpringBootApplicationMain classEnables auto-config, component scanning, configuration
@ComponentAny classGeneric Spring-managed bean
@ServiceService layer classSemantic alias for @Component — marks business logic
@RepositoryData access classAlias + wraps SQL exceptions into Spring's DataAccessException
@RestControllerHTTP handler class@Controller + @ResponseBody — JSON returned from every method
@ConfigurationConfig classDeclares @Bean methods — factory for Spring beans
@BeanMethod in @ConfigurationReturn value registered as a Spring bean
@AutowiredConstructor / fieldInjects a dependency (optional on single-constructor classes)
@ValueField / paramInjects a value from properties: @Value("${app.name}")
@ProfileClass / methodActivate bean only for specific profiles (dev, prod)
Java — @Configuration with @Bean
@Configuration public class AppConfig { // Declare a bean for a third-party class you can't annotate directly @Bean public ObjectMapper objectMapper() { return new ObjectMapper() .registerModule(new JavaTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } // Profile-specific bean — only loaded when profile = "dev" @Bean @Profile("dev") public DataSource h2DataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .build(); } }
06

Configuration

// APPLICATION.YML AND PROFILES

Spring Boot reads application.yml (or .properties) from src/main/resources. Profiles let you maintain different configs for development, staging, and production without changing code.

YAML — application.yml
# application.yml — base config (all environments) spring: application: name: bookstore-api datasource: url: ${DB_URL:jdbc:h2:mem:devdb} # env var with fallback username: ${DB_USER:sa} password: ${DB_PASS:} jpa: hibernate: ddl-auto: validate # NEVER 'create' or 'update' in prod show-sql: false properties: hibernate: format_sql: true server: port: 8080 logging: level: com.example.bookstore: DEBUG org.springframework.web: INFO # Custom properties — bind to @ConfigurationProperties class bookstore: max-results-per-page: 50 cache-ttl-minutes: 10
Java — @ConfigurationProperties (type-safe config binding)
@ConfigurationProperties(prefix = "bookstore") @Component public class BookstoreProperties { private int maxResultsPerPage = 20; // default if not set private int cacheTtlMinutes = 5; // getters + setters (or use Lombok @Data) } // Inject and use: @Service public class BookService { private final BookstoreProperties props; // props.getMaxResultsPerPage() → 50 (from yml) }
ddl-auto in production: Never use spring.jpa.hibernate.ddl-auto: create or update on a production database. Use validate (checks schema matches entities) or none, and manage schema with Flyway or Liquibase migrations.
07

REST Controllers

// BUILDING HTTP ENDPOINTS

@RestController combines @Controller and @ResponseBody, meaning every method's return value is serialized to JSON automatically. Map HTTP methods to Java methods with @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping.

Java — Full REST Controller
@RestController @RequestMapping("/api/v1/books") @RequiredArgsConstructor // Lombok — generates constructor injection public class BookController { private final BookService bookService; // GET /api/v1/books @GetMapping public ResponseEntity<List<BookResponse>> getAllBooks( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { return ResponseEntity.ok(bookService.findAll(page, size)); } // GET /api/v1/books/{id} @GetMapping("/{id}") public ResponseEntity<BookResponse> getBook(@PathVariable Long id) { return ResponseEntity.ok(bookService.findById(id)); } // POST /api/v1/books @PostMapping public ResponseEntity<BookResponse> createBook( @Valid @RequestBody BookRequest request) { BookResponse created = bookService.create(request); URI location = ServletUriComponentsBuilder .fromCurrentRequest() .path("/{id}") .buildAndExpand(created.getId()) .toUri(); return ResponseEntity.created(location).body(created); // 201 Created } // PUT /api/v1/books/{id} @PutMapping("/{id}") public ResponseEntity<BookResponse> updateBook( @PathVariable Long id, @Valid @RequestBody BookRequest request) { return ResponseEntity.ok(bookService.update(id, request)); } // DELETE /api/v1/books/{id} @DeleteMapping("/{id}") public ResponseEntity<Void> deleteBook(@PathVariable Long id) { bookService.delete(id); return ResponseEntity.noContent().build(); // 204 No Content } }
08

Request Handling

// EXTRACTING DATA FROM HTTP REQUESTS
AnnotationSourceExample
@PathVariableURL path segmentGET /books/{id}@PathVariable Long id
@RequestParamQuery stringGET /books?page=2&size=10
@RequestBodyJSON request bodyPOST /books with JSON payload
@RequestHeaderHTTP header@RequestHeader("X-API-Key") String key
@CookieValueCookie@CookieValue("sessionId") String session
@ModelAttributeForm data / multiple paramsBinds form fields to a POJO

DTOs — never expose your entities directly

Return DTOs (Data Transfer Objects) from your controllers, not JPA entities. Entities may contain sensitive fields, lazy-loaded associations, or Hibernate proxy objects that serialize poorly. DTOs give you full control over the API contract.

Java — Request and Response DTOs
// Input DTO — what the client sends public record BookRequest( @NotBlank(message = "Title is required") String title, @NotBlank String author, @NotNull @DecimalMin("0.01") BigDecimal price, @ISBN // validates ISBN format String isbn ) {} // Output DTO — what the API returns public record BookResponse( Long id, String title, String author, BigDecimal price, String isbn, LocalDateTime createdAt ) { // Static factory — convert entity to DTO public static BookResponse from(Book book) { return new BookResponse(book.getId(), book.getTitle(), book.getAuthor(), book.getPrice(), book.getIsbn(), book.getCreatedAt()); } }
09

Validation & Error Handling

// @VALID, BEAN VALIDATION, GLOBAL EXCEPTION HANDLER

Spring Boot integrates Bean Validation (Jakarta Validation). Add @Valid before a @RequestBody parameter and Spring automatically validates the object — returning a 400 if any constraint fails. Customize error responses with a @ControllerAdvice.

Java — Global Exception Handler
@RestControllerAdvice // applies to all @RestController classes public class GlobalExceptionHandler { // 400 — Validation failures (@Valid) @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, Object> handleValidation(MethodArgumentNotValidException ex) { Map<String, String> fieldErrors = new LinkedHashMap<>(); ex.getBindingResult().getFieldErrors().forEach(err -> fieldErrors.put(err.getField(), err.getDefaultMessage())); return Map.of("status", 400, "errors", fieldErrors); } // 404 — Custom not-found exception @ExceptionHandler(BookNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public Map<String, Object> handleNotFound(BookNotFoundException ex) { return Map.of("status", 404, "message", ex.getMessage()); } // 500 — Catch-all for unexpected errors @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Map<String, Object> handleGeneral(Exception ex) { return Map.of("status", 500, "message", "An unexpected error occurred"); // Never expose ex.getMessage() in production — reveals internals } }
Never expose stack traces: Spring Boot's default error response at /error includes the exception message and sometimes a stack trace. Disable this in production with server.error.include-stacktrace=never and server.error.include-message=never in your production profile.
10

Spring Data JPA

// ENTITIES AND THE REPOSITORY PATTERN

Spring Data JPA removes the boilerplate of writing DAO classes. Define an interface extending JpaRepository and Spring generates the implementation at runtime. All CRUD operations — save, findById, findAll, delete — are available out of the box.

Java — JPA Entity
@Entity @Table(name = "books") public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 255) private String title; @Column(nullable = false) private String author; @Column(precision = 10, scale = 2) private BigDecimal price; @Column(unique = true, length = 13) private String isbn; @CreationTimestamp // Hibernate sets this on INSERT private LocalDateTime createdAt; @ManyToOne(fetch = FetchType.LAZY) // LAZY — don't auto-join unless needed @JoinColumn(name = "category_id") private Category category; // getters + setters (or use Lombok @Getter @Setter) }
FetchType.LAZY: Always use LAZY for @OneToMany and @ManyToOne associations. EAGER loading (the default for @ManyToOne) fires additional SQL queries automatically, even when you don't need the associated data — a common source of N+1 query problems.
11

Queries & Repositories

// DERIVED QUERIES, JPQL, AND NATIVE SQL
Java — Repository with query methods
@Repository public interface BookRepository extends JpaRepository<Book, Long> { // 1. Derived query — Spring generates SQL from the method name List<Book> findByAuthorIgnoreCase(String author); List<Book> findByPriceLessThanEqual(BigDecimal maxPrice); Optional<Book> findByIsbn(String isbn); boolean existsByIsbn(String isbn); // 2. JPQL — object-oriented query language, references entity/field names @Query("SELECT b FROM Book b WHERE b.title LIKE %:keyword% OR b.author LIKE %:keyword%") List<Book> search(@Param("keyword") String keyword); // 3. Native SQL — escape hatch when JPA won't do @Query(value = "SELECT * FROM books WHERE price BETWEEN :min AND :max ORDER BY price", nativeQuery = true) List<Book> findInPriceRange(@Param("min") BigDecimal min, @Param("max") BigDecimal max); // 4. Pagination — built-in with Pageable Page<Book> findByCategory_Name(String categoryName, Pageable pageable); // 5. Custom update with @Modifying @Modifying @Query("UPDATE Book b SET b.price = b.price * :factor WHERE b.category.id = :catId") int applyPriceMultiplier(@Param("factor") BigDecimal factor, @Param("catId") Long catId); }

Using Pageable

Java — Pagination in Service
// Service method public Page<BookResponse> findAll(int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("title").ascending()); return bookRepository.findAll(pageable).map(BookResponse::from); } // Controller maps it — Page<T> serializes to JSON with metadata: // { "content": [...], "totalElements": 143, "totalPages": 8, // "number": 0, "size": 20, "first": true, "last": false }
12

Database Migrations

// FLYWAY — VERSION-CONTROLLED SCHEMAS

Never let Hibernate manage your production schema with ddl-auto. Use Flyway for version-controlled, repeatable database migrations. Add spring-boot-starter-flyway and Spring Boot auto-runs migrations at startup.

SQL — Flyway migration files
-- src/main/resources/db/migration/V1__init.sql CREATE TABLE categories ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE books ( id BIGSERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, author VARCHAR(255) NOT NULL, price DECIMAL(10,2) NOT NULL, isbn VARCHAR(13) UNIQUE, category_id BIGINT REFERENCES categories(id), created_at TIMESTAMP DEFAULT NOW() ); -- src/main/resources/db/migration/V2__add_book_index.sql CREATE INDEX idx_books_author ON books(author); CREATE INDEX idx_books_price ON books(price); -- Naming: V{version}__{description}.sql -- Once run, migration files are IMMUTABLE — never edit a ran migration
13

Service Layer Pattern

// BUSINESS LOGIC AND TRANSACTIONS

The service layer is where business logic lives. Controllers delegate to services; services call repositories. @Transactional ensures database operations within a method run in a single transaction — if anything fails, everything rolls back.

Java — Service with @Transactional
@Service @RequiredArgsConstructor @Transactional(readOnly = true) // default: read-only (optimization) public class BookService { private final BookRepository bookRepository; public Page<BookResponse> findAll(int page, int size) { return bookRepository.findAll(PageRequest.of(page, size)) .map(BookResponse::from); } public BookResponse findById(Long id) { return bookRepository.findById(id) .map(BookResponse::from) .orElseThrow(() -> new BookNotFoundException("Book not found: " + id)); } @Transactional // write operation — overrides class-level readOnly public BookResponse create(BookRequest request) { if (bookRepository.existsByIsbn(request.isbn())) { throw new IllegalArgumentException("ISBN already exists"); } Book book = new Book(); book.setTitle(request.title()); book.setAuthor(request.author()); book.setPrice(request.price()); book.setIsbn(request.isbn()); return BookResponse.from(bookRepository.save(book)); } @Transactional public void delete(Long id) { if (!bookRepository.existsById(id)) { throw new BookNotFoundException("Book not found: " + id); } bookRepository.deleteById(id); } }
14

Testing

// UNIT TESTS, SLICE TESTS, INTEGRATION TESTS

Spring Boot provides test annotations that load only the slices you need — avoiding a full application context when you don't need one. @WebMvcTest for controllers, @DataJpaTest for repositories, and @SpringBootTest for full integration tests.

Unit Tests Fastest

Test a single class in isolation. Mock all dependencies with Mockito. No Spring context. Run in milliseconds. The bulk of your test suite.

Slice Tests Targeted

@WebMvcTest loads only the web layer. @DataJpaTest loads only JPA. Faster than full context, more integration than pure unit tests.

Integration Tests Slowest

@SpringBootTest loads the full application context. Use Testcontainers for a real database. Validates the entire stack together.

Java — Unit Test (Service)
@ExtendWith(MockitoExtension.class) class BookServiceTest { @Mock private BookRepository bookRepository; @InjectMocks private BookService bookService; @Test void findById_existingBook_returnsResponse() { // Arrange Book book = new Book(); book.setId(1L); book.setTitle("Clean Code"); book.setAuthor("Martin"); when(bookRepository.findById(1L)).thenReturn(Optional.of(book)); // Act BookResponse result = bookService.findById(1L); // Assert assertThat(result.title()).isEqualTo("Clean Code"); verify(bookRepository).findById(1L); } @Test void findById_missingBook_throwsException() { when(bookRepository.findById(99L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> bookService.findById(99L)) .isInstanceOf(BookNotFoundException.class); } }
Java — @WebMvcTest (Controller Slice)
@WebMvcTest(BookController.class) class BookControllerTest { @Autowired private MockMvc mockMvc; @MockBean private BookService bookService; @Autowired private ObjectMapper objectMapper; @Test void getBook_found_returns200() throws Exception { BookResponse resp = new BookResponse(1L, "Clean Code", "Martin", new BigDecimal("39.99"), null, null); when(bookService.findById(1L)).thenReturn(resp); mockMvc.perform(get("/api/v1/books/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.title").value("Clean Code")) .andExpect(jsonPath("$.author").value("Martin")); } }
15

Spring Security Basics

// SECURING YOUR API

Add spring-boot-starter-security and every endpoint requires authentication immediately. Configure the security rules in a SecurityFilterChain bean. For REST APIs, stateless JWT-based auth is the standard.

Java — SecurityFilterChain (Stateless JWT)
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(AbstractHttpConfigurer::disable) // stateless API — no CSRF .sessionManagement(sm -> sm .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/auth/**").permitAll() // public endpoints .requestMatchers(HttpMethod.GET, "/api/v1/books/**").permitAll() .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") // role-guarded .anyRequest().authenticated() // everything else needs auth ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // always use BCrypt for passwords } }
Method-level security: Enable @EnableMethodSecurity on your config class to use @PreAuthorize("hasRole('ADMIN')") directly on service methods — a fine-grained alternative to URL-level rules that keeps authorization logic close to the business logic it protects.
16

Actuator & Observability

// HEALTH, METRICS, AND MONITORING

Spring Boot Actuator exposes production-ready endpoints for health checks, metrics, and application info. Add spring-boot-starter-actuator. By default only /actuator/health and /actuator/info are exposed — configure carefully.

YAML — Actuator configuration
management: endpoints: web: exposure: include: health,info,metrics,prometheus # expose only what you need # NEVER include 'env' or 'heapdump' in production endpoint: health: show-details: when-authorized # DB, disk, external services metrics: tags: application: ${spring.application.name} environment: ${APP_ENV:development} # Result: # GET /actuator/health → {"status":"UP","components":{...}} # GET /actuator/metrics → list of available metric names # GET /actuator/metrics/http.server.requests → request count, duration # GET /actuator/prometheus → Prometheus scrape format
EndpointURLUseful For
health/actuator/healthKubernetes liveness/readiness probes
info/actuator/infoApp version, git commit, build info
metrics/actuator/metrics/{name}JVM, HTTP, custom Micrometer metrics
prometheus/actuator/prometheusPrometheus scrape target for Grafana dashboards
loggers/actuator/loggersChange log levels at runtime without restart
17

Packaging & Deployment

// JAR, DOCKER, AND ENVIRONMENT CONFIG
Dockerfile — Multi-stage build
# Stage 1: Build FROM eclipse-temurin:21-jdk-alpine AS build WORKDIR /app COPY .mvn/ .mvn/ COPY mvnw pom.xml ./ RUN ./mvnw dependency:go-offline # cache deps layer COPY src ./src RUN ./mvnw package -DskipTests # Stage 2: Runtime — lean image, no JDK FROM eclipse-temurin:21-jre-alpine WORKDIR /app # Use layered JAR for better Docker cache utilization ARG JAR_FILE=target/*.jar COPY --from=build /app/${JAR_FILE} app.jar # Non-root user for security RUN addgroup -S spring && adduser -S spring -G spring USER spring:spring EXPOSE 8080 ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
YAML — application-prod.yml (production profile)
# Activated with: -Dspring.profiles.active=prod OR SPRING_PROFILES_ACTIVE=prod spring: datasource: url: ${DB_URL} # required env vars — no fallback in prod username: ${DB_USER} password: ${DB_PASS} hikari: maximum-pool-size: 20 minimum-idle: 5 jpa: show-sql: false hibernate: ddl-auto: validate server: error: include-stacktrace: never include-message: never logging: level: root: WARN com.example.bookstore: INFO
18

Patterns & Common Pitfalls

// WHAT TRIPS UP BEGINNERS
Common Pitfalls Avoid
  • Field injection — use constructor injection
  • Exposing entities as API responses — use DTOs
  • ddl-auto: update in production — use Flyway
  • N+1 queries — LAZY + JOIN FETCH or projections
  • No @Transactional on write operations — data may not persist
  • Catching and swallowing exceptions — use @ControllerAdvice
  • Logging secrets/tokens — review all log statements
  • Hardcoded passwords in application.yml — use env vars
  • Missing input validation — always use @Valid
  • Returning 200 for created resources — use 201 with Location
Best Practices Follow
  • Layer separation — Controller → Service → Repository
  • Constructor injection with final fields
  • Record DTOs for immutable request/response objects
  • @Transactional(readOnly=true) at class level; override for writes
  • Custom exceptions + GlobalExceptionHandler
  • Profiles for dev/staging/prod configuration
  • Flyway for schema migrations
  • Pagination on all list endpoints (never return unbounded lists)
  • Actuator health wired to K8s readiness/liveness probes
  • Secrets from environment variables, not config files

Solving the N+1 Problem

The N+1 problem occurs when fetching N books also fires N extra queries to fetch each book's category. Fix it with a JOIN FETCH in JPQL or with an @EntityGraph.

Java — Fix N+1 with JOIN FETCH
// ❌ N+1: each book triggers a SELECT for its category List<Book> books = bookRepository.findAll(); // 1 query + N queries for categories // ✅ Fix 1: JPQL JOIN FETCH — single query @Query("SELECT b FROM Book b JOIN FETCH b.category") List<Book> findAllWithCategory(); // ✅ Fix 2: @EntityGraph — declarative eager loading @EntityGraph(attributePaths = {"category"}) List<Book> findAll(); // ✅ Fix 3: Projections — only fetch what you need public interface BookSummary { String getTitle(); String getAuthor(); BigDecimal getPrice(); // No category — no extra join at all } List<BookSummary> findAllProjectedBy(); // SELECT title, author, price only
Enable SQL logging in dev: Set spring.jpa.show-sql: true and spring.jpa.properties.hibernate.format_sql: true during development to see every query. Use p6spy or datasource-proxy to also see bind parameters. Review the output — N+1 patterns are immediately visible when you look at the logs.
TopicQuick Reference
HTTP Status codes200 OK · 201 Created · 204 No Content · 400 Bad Request · 401 Unauthorized · 403 Forbidden · 404 Not Found · 422 Unprocessable · 500 Internal Server Error
Common startersweb · data-jpa · security · validation · test · actuator · cache · mail · data-redis
Useful Spring annotations@Scheduled (cron jobs) · @Async (async methods) · @Cacheable (caching) · @EventListener (app events)
Test annotations@SpringBootTest · @WebMvcTest · @DataJpaTest · @MockBean · @TestPropertySource
Reference docsdocs.spring.io/spring-boot · start.spring.io · spring.io/guides · baeldung.com/spring-boot