Spring Boot: Auto-Configuration, REST APIs, JPA & Security

Spring Boot: Auto-Configuration, REST APIs, JPA & Security

Spring Boot has been the #1 Java backend framework for years - and in 2026, it's more dominant than ever. It powers fintech platforms, e-commerce backends, healthcare APIs, and enterprise microservices across industries. If you're building Java backends professionally, Spring Boot is not optional knowledge - it's the baseline.

But Spring Boot's reputation for being "easy to start, hard to master" is well-earned. The starter projects get you to a running server in minutes. The confusion starts when you try to understand why it works - why auto-configuration kicks in, how JPA entities map to database tables, how Spring Security 6's filter chain actually secures your endpoints, and where JWT tokens fit into the authentication flow. Most tutorials show you what to type. This guide explains what's happening as you type it.

This guide covers the complete Spring Boot stack - from auto-configuration and application.yml through JPA and REST APIs to Spring Security 6 with JWT authentication. Every section includes real, production-style code. By the end, you'll have a clear mental model of how all the pieces fit together into a complete, secured backend application. If you need to strengthen your Java and Spring MVC foundation first, Board Infinity's guides on core Java concepts and syntax and OOP concepts in Java are the ideal starting points - Spring Boot's entire dependency injection and annotation model is built on Java's class, interface, and object system.

Who This Guide Is For

This guide is for developers who:

  • Know Java basics and have some Spring MVC exposure
  • Want a complete, end-to-end Spring Boot reference for 2026
  • Have started Spring Boot tutorials but want to understand the full picture
  • Are building their first production-grade Spring Boot API
  • Want to understand how Spring Boot fits into the broader Java backend ecosystem - Board Infinity's Essentials of Back-End Development: From APIs to Databases covers the full backend architecture picture that Spring Boot implements

1. Spring Boot Auto-Configuration: How It Removes Boilerplate

The most transformative thing Spring Boot does is auto-configuration - and most developers use it daily without understanding the mechanism. When you add spring-boot-starter-web to your pom.xml, Spring Boot automatically configures an embedded Tomcat server, a DispatcherServlet, Jackson for JSON serialisation, and dozens of sensible defaults - all without a single XML file or @Bean declaration from you.

The mechanism is @EnableAutoConfiguration, which is included in @SpringBootApplication. At startup, Spring Boot scans META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in every JAR on the classpath, finds auto-configuration classes, and applies them conditionally - only if the required classes are present and the bean isn't already configured by the developer.

The key insight: auto-configuration is conditional. DataSourceAutoConfiguration only applies if you have a database driver on the classpath. SecurityAutoConfiguration only applies if you have spring-security-web present. You can override any auto-configured bean by declaring your own. Your declaration takes priority. This is the "convention over configuration" philosophy in action. The Java class and annotation system that makes this possible - specifically how @Configuration classes and @Bean methods work - is rooted in the classes and objects model covered in Board Infinity's classes and objects in Java guide.

Java - @SpringBootApplication and Auto-Configuration
// @SpringBootApplication = 3 annotations combined:
// @Configuration + @EnableAutoConfiguration + @ComponentScan
@SpringBootApplication
public class StoreApplication {
    public static void main(String[] args) {
        SpringApplication.run(StoreApplication.class, args);
    }
}// What auto-configuration gives you for FREE with spring-boot-starter-web:
// - Embedded Tomcat on port 8080
// - DispatcherServlet mapped to "/"
// - Jackson ObjectMapper for JSON
// - Default error handling at /error
// - ContentNegotiation (JSON by default)// Overriding auto-config: your @Bean takes priority over auto-config
@Configuration
public class JacksonConfig {@Bean
public ObjectMapper objectMapper() {
    // Your custom ObjectMapper replaces Spring Boot's auto-configured one
    return new ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
}
๐Ÿ’ก
Debug Auto-Configuration With --debug Flag

Run your Spring Boot app with --debug flag or add debug=true to application.yml. Spring Boot prints a "CONDITIONS EVALUATION REPORT" showing every auto-configuration class, whether it was applied, and if not, why. This is invaluable when a feature isn't working as expected - it shows exactly which condition failed to activate the auto-config you expected.

2. Starter Dependencies - What They Are and When to Use Them

Starter dependencies are Spring Boot's curated dependency bundles. Instead of adding 8 separate Maven dependencies for a web application, you add one: spring-boot-starter-web. Spring Boot manages compatible versions and brings in everything that technology needs to work together.

Every starter follows the naming convention spring-boot-starter-{technology}. The most common starters cover web, data, security, validation, testing, and messaging. Understanding what each starter includes helps you avoid adding conflicting dependencies and explains why certain auto-configurations activate. The interface-based architecture that makes starters composable - where spring-boot-starter-security brings in Spring Security's filter interfaces that you then implement - is covered in Board Infinity's multiple inheritance in Java guide, which explains how Java interfaces allow different starters to plug into Spring's unified application context without conflicts.

Starter What It Includes Auto-Configures Use When
spring-boot-starter-web Spring MVC, Tomcat, Jackson DispatcherServlet, JSON serialisation Building REST APIs or web apps
spring-boot-starter-data-jpa Hibernate, Spring Data JPA, JDBC DataSource, EntityManagerFactory, repositories Connecting to a relational database
spring-boot-starter-security Spring Security, Spring Security Web SecurityFilterChain, basic auth by default Adding authentication and authorisation
spring-boot-starter-validation Hibernate Validator, Jakarta Validation Method validation, @Valid support Validating request bodies and method parameters
spring-boot-starter-test JUnit 5, Mockito, AssertJ, MockMvc Test slices (@WebMvcTest, @DataJpaTest) Writing unit and integration tests

3. application.yml Deep Dive - Profiles, Properties, and Bindings

application.yml (or application.properties) is where you configure your Spring Boot application's runtime behaviour - database URLs, server ports, security settings, custom properties, and environment-specific overrides. Understanding its structure and the profile system is essential for managing development, staging, and production configurations cleanly.

Spring Profiles let you define environment-specific configuration in the same file (or separate files) and activate the right one per environment. The spring.profiles.active property determines which profile is active - set it in the YAML, as an environment variable, or as a command-line argument. The @ConfigurationProperties binding classes - JwtProperties and PaginationProperties - use private fields with getters and setters. This is the encapsulation principle directly applied to configuration management. Board Infinity's guide on abstraction vs encapsulation in Java explains why this pattern matters: the jwtSecret field in JwtProperties must be private - exposing it publicly would make the signing secret accessible to any component that injects the properties class, creating a security leak.

YAML - application.yml with Profiles and @ConfigurationProperties
# application.yml - shared config for all environments
spring:
  application:
    name: store-api
  profiles:
    active: development  # override with env var: SPRING_PROFILES_ACTIVE=production
server:
port: 8080
app:
jwt:
secret: "your-256-bit-secret-key-here"
expiry-ms: 86400000  # 24 hours
pagination:
default-size: 20
max-size: 100

# Development profile - uses H2 in-memory database
spring:
config:
activate:
on-profile: development
datasource:
url: jdbc:h2:mem:storedb
driver-class-name: org.h2.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop

# Production profile - uses PostgreSQL
spring:
config:
activate:
on-profile: production
datasource:
url: ${DATABASE_URL}         # reads from environment variable
username: ${DATABASE_USER}
password: ${DATABASE_PASS}
jpa:
show-sql: false
hibernate:
ddl-auto: validate          # never auto-create in production
Java - Binding app.jwt and app.pagination to Typed Classes
// Bind app.jwt.* properties to this class
@ConfigurationProperties(prefix = "app.jwt")
public class JwtProperties {
    private String secret;
    private long   expiryMs;
public String getSecret()   { return secret; }
public long   getExpiryMs() { return expiryMs; }
public void   setSecret(String s)   { this.secret = s; }
public void   setExpiryMs(long ms)  { this.expiryMs = ms; }
}
// Bind app.pagination.* properties
@ConfigurationProperties(prefix = "app.pagination")
public class PaginationProperties {
private int defaultSize = 20;
private int maxSize     = 100;
public int getDefaultSize() { return defaultSize; }
public int getMaxSize()     { return maxSize; }
public void setDefaultSize(int s) { this.defaultSize = s; }
public void setMaxSize(int m)     { this.maxSize = m; }
}
// Enable both in @SpringBootApplication class
@SpringBootApplication
@EnableConfigurationProperties({JwtProperties.class, PaginationProperties.class})
public class StoreApplication { /* ... */ }
โš ๏ธ
Never Use ddl-auto: create or create-drop in Production

spring.jpa.hibernate.ddl-auto controls whether Hibernate modifies your database schema on startup. create drops and recreates all tables. create-drop drops everything on shutdown. These are only safe for development. In production, use validate (verifies schema matches entities without changing anything) or none. Manage production schema changes with a migration tool like Flyway or Liquibase.

4. Connecting to Databases: JPA, Hibernate, and Spring Data

Spring Data JPA is Spring Boot's database abstraction layer. It combines two technologies: JPA (Jakarta Persistence API) - the specification for object-relational mapping - and Hibernate - the most widely used JPA implementation. Together they let you define Java classes as database table mappings, write zero-SQL queries using method names, and manage database relationships through annotations.

The three components you work with every day are: @Entity classes (your Java representation of database tables), JpaRepository interfaces (your data access layer), and @Transactional (controlling when database operations commit or roll back).

The JpaRepository<Product, Long> generic type parameters - where Product is the entity type and Long is the ID type - are Java generics in direct action. Board Infinity's generics in Java guide explains what these angle brackets mean: <T, ID> defines the type contract for the entire repository interface, and understanding generics makes every repository declaration self-explanatory rather than mysterious boilerplate. The List<Product> return types from derived query methods, and the Page<Product> pagination return types, are also generics throughout. The collections used in JPA - List, Set for entity relationships - connect directly to Board Infinity's Java List guide and HashSet in Java guide.

Java - Entity, Repository and Service Layer
// Entity - maps to "products" table in the database
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 100)
private String name;

@Column(nullable = false)
private Double price;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;

@CreationTimestamp
private LocalDateTime createdAt;

// getters and setters...
}
// Repository - Spring Data generates all implementations
public interface ProductRepository extends JpaRepository<Product, Long> {
// Spring generates SQL: SELECT * FROM products WHERE name = ?
List<Product> findByName(String name);

// Spring generates SQL: SELECT * FROM products WHERE price < ? ORDER BY price ASC
List<Product> findByPriceLessThanOrderByPriceAsc(Double maxPrice);

// Custom JPQL query
@Query("SELECT p FROM Product p WHERE p.category.name = :category AND p.price BETWEEN :min AND :max")
Page<Product> findByCategoryAndPriceRange(
    @Param("category") String category,
    @Param("min") Double min,
    @Param("max") Double max,
    Pageable pageable
);
}
// Service with @Transactional
@Service
public class ProductService {
private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
    this.productRepository = productRepository;
}

@Transactional
public Product createProduct(CreateProductRequest req) {
    Product product = new Product();
    product.setName(req.getName());
    product.setPrice(req.getPrice());
    return productRepository.save(product); // INSERT committed on method return
}

public Page<Product> getProducts(int page, int size) {
    return productRepository.findAll(PageRequest.of(page, size));
}
}
๐Ÿ”
Use FetchType.LAZY for @ManyToOne and @OneToMany - Always

By default, @ManyToOne uses FetchType.EAGER - which loads the related entity immediately every time you load the owning entity. For a Product with a Category, this means every findAll() on products loads all their categories too. In a list of 1000 products, that's 1000 extra queries (the N+1 problem). Use FetchType.LAZY and let the service layer explicitly load relationships when needed via @EntityGraph or JOIN FETCH queries.

5. Building Full REST APIs: Service - Controller - Repository

A well-structured Spring Boot REST API has three clearly separated layers: the repository layer handles all database access, the service layer contains all business logic and orchestration, and the controller layer handles HTTP - routing, request parsing, response formatting, and validation. Each layer has one responsibility and communicates with the layer below it.

This separation makes testing, debugging, and extending the application dramatically easier. When a bug occurs, you know exactly which layer to look in. When a requirement changes, you know exactly which layer to modify.

The Page.map(this::toDto) stream transformation in the service layer - converting a Page<Product> to Page<ProductDto> using a method reference - is the same functional transformation pattern covered in Board Infinity's map stream in Java guide. The .orElseThrow(() -> new CategoryNotFoundException(...)) Optional pattern used when loading the category is covered in Board Infinity's throw and throws in Java guide - understanding the distinction between checked and unchecked exceptions determines whether Spring's @Transactional rolls back on your custom exception automatically (it does for unchecked RuntimeException subclasses, not for checked exceptions by default).

Java - Complete 3-Layer API: Controller - Service - Repository
// DTO - data transfer object for API request/response
public record ProductDto(Long id, String name, Double price, String category) {}
public record CreateProductRequest(
@NotBlank String name,
@Positive Double price,
@NotNull  Long   categoryId
) {}
// CONTROLLER LAYER - HTTP handling only
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;

public ProductController(ProductService productService) {
    this.productService = productService;
}

@GetMapping
public Page<ProductDto> getAll(
        @RequestParam(defaultValue = "0")  int page,
        @RequestParam(defaultValue = "20") int size) {
    return productService.getProducts(page, size);
}

@PostMapping
public ResponseEntity<ProductDto> create(
        @Valid @RequestBody CreateProductRequest request) {
    ProductDto created = productService.create(request);
    return ResponseEntity.status(201).body(created);
}
}
// SERVICE LAYER - business logic and DTO mapping
@Service
public class ProductService {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;

public ProductService(ProductRepository pr, CategoryRepository cr) {
    this.productRepository  = pr;
    this.categoryRepository = cr;
}

public Page<ProductDto> getProducts(int page, int size) {
    return productRepository
        .findAll(PageRequest.of(page, size))
        .map(this::toDto); // map entity to DTO
}

@Transactional
public ProductDto create(CreateProductRequest req) {
    Category category = categoryRepository.findById(req.categoryId())
        .orElseThrow(() -> new CategoryNotFoundException(req.categoryId()));

    Product product = new Product();
    product.setName(req.name());
    product.setPrice(req.price());
    product.setCategory(category);

    return toDto(productRepository.save(product));
}

private ProductDto toDto(Product p) {
    return new ProductDto(p.getId(), p.getName(), p.getPrice(),
        p.getCategory() != null ? p.getCategory().getName() : null);
}
}

6. Spring Security 6: Authentication, Roles, and JWT

Spring Security 6 (released with Spring Boot 3) introduced a cleaner, lambda-based configuration approach that replaces the deprecated WebSecurityConfigurerAdapter. The core concept remains the same: a SecurityFilterChain of filters intercepts every HTTP request and enforces your security rules before the request reaches your controller.

The standard authentication flow for a modern REST API is: the client sends credentials (username + password) to a login endpoint, the server validates them, generates a JWT token, and returns it. For subsequent requests, the client includes the JWT in the Authorization: Bearer {token} header. A custom filter validates the token and sets the security context before the request reaches the controller.

The JwtAuthFilter extends OncePerRequestFilter and @Override protected void doFilterInternal(...) pattern uses Java's method override mechanism directly. Board Infinity's overloading vs overriding guide covers why @Override matters here: if the method signature doesn't match OncePerRequestFilter's abstract method exactly, without @Override the annotation would compile silently as a new method rather than an override - and the JWT filter would never run. The UserDetailsService interface that UserDetailsServiceImpl implements is the interface-based abstraction covered in Board Infinity's abstraction in Java guide - Spring Security calls loadUserByUsername() through the interface, completely decoupled from your specific database implementation. The SecurityContextHolder that stores authenticated user per thread uses the Singleton pattern covered in Board Infinity's singleton class in Java guide.

Java - Spring Security 6 Configuration (Lambda Style)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // enables @PreAuthorize on methods
public class SecurityConfig {
private final JwtAuthFilter           jwtAuthFilter;
private final UserDetailsServiceImpl   userDetailsService;

public SecurityConfig(JwtAuthFilter jwtFilter,
                       UserDetailsServiceImpl uds) {
    this.jwtAuthFilter    = jwtFilter;
    this.userDetailsService = uds;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())        // REST APIs don't need CSRF
        .sessionManagement(sm -> sm
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // no sessions - JWT
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/auth/**").permitAll()   // login/register open
            .requestMatchers("/api/public/**").permitAll() // public endpoints
            .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() // read-only public
            .requestMatchers("/api/admin/**").hasRole("ADMIN") // admin only
            .anyRequest().authenticated())               // everything else needs auth
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
        .build();
}

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(); // never store plain-text passwords
}

@Bean
public AuthenticationManager authManager(AuthenticationConfiguration config)
        throws Exception {
    return config.getAuthenticationManager();
}
}
Java - JWT Utility and Authentication Filter
// JWT utility - generates and validates tokens
@Component
public class JwtUtils {
private final JwtProperties jwtProperties;

public JwtUtils(JwtProperties jwtProperties) {
    this.jwtProperties = jwtProperties;
}

public String generateToken(UserDetails userDetails) {
    return Jwts.builder()
        .subject(userDetails.getUsername())
        .issuedAt(new Date())
        .expiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiryMs()))
        .signWith(getSignKey())
        .compact();
}

public String  extractUsername(String token) {
    return extractClaims(token).getSubject();
}

public boolean isTokenValid(String token, UserDetails userDetails) {
    final String username = extractUsername(token);
    return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}

private boolean isTokenExpired(String token) {
    return extractClaims(token).getExpiration().before(new Date());
}

private Claims extractClaims(String token) {
    return Jwts.parser()
        .verifyWith(getSignKey())
        .build()
        .parseSignedClaims(token)
        .getPayload();
}

private SecretKey getSignKey() {
    byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecret());
    return Keys.hmacShaKeyFor(keyBytes);
}
}
// JWT filter - runs on every request before authentication
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtils             jwtUtils;
private final UserDetailsService   userDetailsService;

public JwtAuthFilter(JwtUtils jwtUtils, UserDetailsService uds) {
    this.jwtUtils           = jwtUtils;
    this.userDetailsService = uds;
}

@Override
protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain chain) throws IOException, ServletException {

    final String authHeader = request.getHeader("Authorization");

    if (authHeader == null || !authHeader.startsWith("Bearer ")) {
        chain.doFilter(request, response); // no token - continue without auth
        return;
    }

    final String jwt      = authHeader.substring(7);
    final String username = jwtUtils.extractUsername(jwt);

    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        if (jwtUtils.isTokenValid(jwt, userDetails)) {
            UsernamePasswordAuthenticationToken authToken =
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
    }
    chain.doFilter(request, response);
}
}
โš ๏ธ
Store JWT Secret as Environment Variable - Never in Code

Your JWT signing secret must never be hardcoded in source code or committed to version control. Use an environment variable (APP_JWT_SECRET) and reference it in application.yml as ${APP_JWT_SECRET}. The secret should be at least 256 bits (32 bytes) long for HMAC-SHA256. Generate one with: openssl rand -base64 32. In production, use a secrets manager (AWS Secrets Manager, HashiCorp Vault) rather than environment variables.

7. The Complete Request Lifecycle: From HTTP Request to JSON Response

Understanding how all the layers connect is what separates a developer who can follow tutorials from one who can debug and extend a Spring Boot application confidently. Here's the complete flow for an authenticated API request:

Step 1 - HTTP Request arrives at the embedded Tomcat server.

Step 2 - Security Filter Chain runs. The JwtAuthFilter reads the Authorization header, validates the JWT, and sets the authenticated user in the security context.

Step 3 - DispatcherServlet routes the request to the matching @RestController method.

Step 4 - @Valid triggers Bean Validation on the @RequestBody.

Step 5 - Controller calls Service - no business logic in the controller.

Step 6 - Service runs business logic, calls the repository, maps entities to DTOs, applies @Transactional boundaries.

Step 7 - Repository queries the database via Spring Data JPA and Hibernate.

Step 8 - Response travels back through the layers: DTO serialised to JSON by Jackson, wrapped in ResponseEntity, sent back through Tomcat.

The entire request lifecycle - from HTTP arrival through security, through the three layers, to JSON response - flows through Java's servlet model at every step. Board Infinity's understanding servlets in Java guide explains the Java servlet foundation that Spring Boot's embedded Tomcat, DispatcherServlet, and JwtAuthFilter all build on. The understanding wrapper class in Java guide is also relevant throughout this stack - Long (not long) in entity IDs, Double (not double) in price fields, and Integer pagination parameters all use Java's wrapper types for the nullability that JPA and Spring's web layer require.

Further Reading

Board Infinity Guides:

External Resources:

๐Ÿš€ Build a Complete Secured Spring Boot App

Spring Boot, Spring Security & Application Finalization on Coursera

This free Coursera course by Board Infinity takes every concept in this guide and applies it through a complete, end-to-end project build. You'll go from a blank Spring Boot project to a fully secured REST API with JPA, pagination, JWT authentication, and production-ready code quality.

Module 1
Spring Boot Foundations Auto-configuration, starter dependencies, application.yml with profiles, JPA entities, relationships and Spring Data repositories
Module 2
Building Full REST APIs with Spring Boot Service contracts, business logic, pagination and sorting, DTO mapping, response formatting and practical API design patterns
Module 3
Spring Security & JWT Spring Security 6 filter chain, password encoding, role-based access, @PreAuthorize, JWT generation, validation and endpoint security
Module 4
Final Application Build Full MVC + service + repository integration, validation with security, complete request lifecycle, logging, refactoring and real-world prep
Start Learning on Coursera โ†’

โœ“ Certificate available  ยท  โœ“ Self-paced  ยท  โœ“ Beginner-friendly

Conclusion

Spring Boot's power comes from how its components work together as a system. Auto-configuration removes boilerplate by making opinionated defaults that you override only when needed. Starter dependencies ensure compatible versions and activate the right auto-configurations. application.yml with profiles gives you clean environment management. Spring Data JPA removes the need to write SQL for standard operations. The three-layer architecture keeps concerns separated and code maintainable. Spring Security 6 with JWT provides stateless authentication that scales.

The developers who build production-grade Spring Boot applications quickly are not the ones who memorised the most annotations. They're the ones who understand how each layer communicates with the next - from the incoming HTTP request, through the security filter chain, down through controller and service to the database, and back up as a clean JSON response. That mental model is what this guide has walked through.

With this foundation - auto-configuration, JPA, REST APIs, and Spring Security 6 with JWT - you have the complete toolkit for building professional Java backends in 2026. The next steps from here are testing (unit tests with Mockito, integration tests with @SpringBootTest), containerisation with Docker, and deployment - all of which build directly on the architecture established in this guide. The Java fundamentals that underpin every layer of this stack - polymorphism, encapsulation, generics, collections, and exception handling - are covered comprehensively in Board Infinity's understanding polymorphism in Java guide, which explains the core OOP principle that Spring's DI system, interface-based architecture, and method override patterns all rely on.

Java Programming Java Spring Boot REST API JPA