Skip to main content

Backend Fundamentals: Spring Boot Deep Dive

Introduction

This guide explains every Spring Boot concept used in your e-commerce application. By the end, you'll understand WHY and HOW Spring Boot works, not just what it does.


Table of Contents

  1. What is Spring Boot?
  2. Dependency Injection (The Heart of Spring)
  3. Annotations Explained
  4. Bean Lifecycle
  5. Spring Data JPA
  6. Spring Security
  7. Transaction Management
  8. Exception Handling

What is Spring Boot?

The Problem Spring Boot Solves

Imagine building a house. You need:

  • Plumbing
  • Electricity
  • Foundation
  • Walls

Spring Boot is like a pre-built house framework. Instead of:

  • Setting up a database connection manually
  • Configuring security from scratch
  • Writing server configuration
  • Managing object creation

Spring Boot provides:

  • Auto-configuration: Automatically sets up common patterns
  • Embedded server: No need for external Tomcat
  • Starter dependencies: Pre-configured package bundles
  • Production-ready features: Health checks, metrics, logging

In Your Project

@SpringBootApplication
public class AutomotiveEcommerceApplication {
public static void main(String[] args) {
SpringApplication.run(AutomotiveEcommerceApplication.class, args);
}
}

What happens here?

  1. @SpringBootApplication combines three annotations:

    • @Configuration - This class can define beans
    • @EnableAutoConfiguration - Spring Boot's magic
    • @ComponentScan - Look for components in this package
  2. SpringApplication.run() starts:

    • Embedded Tomcat server (port 8080)
    • Database connection
    • Security configuration
    • Bean creation and wiring

Socratic Question

Q: Why use Spring Boot instead of plain Java?

A: Plain Java requires manual setup of everything. Spring Boot provides conventions and automation, letting you focus on business logic instead of infrastructure.


Dependency Injection

The Core Concept

Problem: How do objects get their dependencies?

Bad Way (Tight Coupling)

public class CartService {
private CartRepository cartRepo = new CartRepository(); // Hard-coded!
private ProductRepository productRepo = new ProductRepository();
}

Issues:

  • Can't change implementations
  • Hard to test (can't mock)
  • Objects tightly coupled
  • Difficult to maintain

Good Way (Dependency Injection)

@Service
public class CartServiceImpl implements CartService {

private final CartRepository cartRepository;
private final ProductRepository productRepository;

@Autowired // Spring injects these!
public CartServiceImpl(CartRepository cartRepository,
ProductRepository productRepository) {
this.cartRepository = cartRepository;
this.productRepository = productRepository;
}
}

Benefits:

  • Loose coupling
  • Easy to test (inject mocks)
  • Flexible (change implementations)
  • Spring manages lifecycle

How It Works

  1. Spring Container (ApplicationContext) creates and manages objects (beans)
  2. When CartServiceImpl is needed, Spring:
    • Creates CartRepository instance
    • Creates ProductRepository instance
    • Injects them into CartServiceImpl constructor
  3. All managed by Spring - you just declare dependencies!

Three Types of Injection

@Autowired
public CartServiceImpl(CartRepository repo) {
this.repo = repo;
}

Benefits:

  • Immutable (final fields)
  • Required dependencies clear
  • Easy to test

2. Setter Injection

@Autowired
public void setCartRepository(CartRepository repo) {
this.repo = repo;
}

Use case:

  • Optional dependencies
  • Allows reconfiguration

3. Field Injection (Avoid ❌)

@Autowired
private CartRepository repo;

Problems:

  • Harder to test
  • Hidden dependencies
  • Can't make final

Socratic Questions

Q1: What if Spring can't find a bean to inject?

A: Application fails to start with NoSuchBeanDefinitionException. Spring validates dependencies at startup.

Q2: How does Spring know which CartRepository implementation to inject if multiple exist?

A: Use @Qualifier("beanName") or @Primary to specify which one.

Q3: Can you inject interface or must be concrete class?

A: Always inject interfaces! This is the whole point - loose coupling. Spring finds the implementation.


Annotations Explained

Stereotype Annotations (Component Scanning)

Spring scans packages looking for these annotations to create beans.

@Component

Generic stereotype - "This is a Spring-managed bean"

@Component
public class EmailService {
// Spring creates and manages this
}

@Service

Business logic layer - Extends @Component with semantic meaning

@Service  // Indicates business service
public class CartServiceImpl implements CartService {
@Transactional // Services handle transactions
public CartDto addToCart(Long userId, AddToCartRequest request) {
// Business logic here
}
}

Why @Service instead of @Component?

  • Semantic clarity (this is business logic)
  • AOP can target @Service specifically
  • Future Spring features may differentiate

@Repository

Data access layer - Extends @Component with exception translation

public interface CartRepository extends JpaRepository<Cart, Long> {
Optional<Cart> findByUserId(Long userId);
}

Special feature: Translates database exceptions to Spring's DataAccessException

@Controller and @RestController

Presentation layer - Handles HTTP requests

@RestController  // @Controller + @ResponseBody
@RequestMapping("/api/cart")
public class CartController {

@GetMapping // Handles GET /api/cart
public ResponseEntity<CartDto> getCart(Authentication auth) {
// Controller logic
}
}

@RestController = @Controller + @ResponseBody on every method

  • @Controller: Marks as web controller
  • @ResponseBody: Converts return value to JSON/XML

Configuration Annotations

@Configuration

Indicates a class provides bean definitions

@Configuration
public class SecurityConfig {

@Bean // Method creates a bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

@Bean

Method-level annotation that produces a bean

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
// Configure and return SecurityFilterChain
return http.build();
}

Mapping Annotations

@RequestMapping Family

@GetMapping("/cart")           // GET request
@PostMapping("/cart/items") // POST request
@PutMapping("/cart/items/{id}") // PUT request
@DeleteMapping("/cart/items/{id}") // DELETE request

@PathVariable

Extracts value from URL path

@DeleteMapping("/items/{itemId}")
public ResponseEntity<CartDto> removeItem(@PathVariable Long itemId) {
// itemId extracted from URL: /items/123 → itemId = 123
}

@RequestBody

Binds HTTP request body to parameter (JSON → Object)

@PostMapping("/cart/items")
public ResponseEntity<CartDto> addToCart(@RequestBody AddToCartRequest request) {
// JSON in request body → AddToCartRequest object
}

@RequestParam

Extracts query parameters

@GetMapping("/products")
public List<Product> getProducts(
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page
) {
// URL: /products?category=Engine&page=2
}

Validation Annotations

@Valid

Triggers validation on object

@PostMapping("/cart/items")
public ResponseEntity<CartDto> addToCart(@Valid @RequestBody AddToCartRequest request) {
// Validates request object before method runs
}

Used with validation annotations on DTO:

public class AddToCartRequest {

@NotNull(message = "Product ID is required")
@Positive(message = "Product ID must be positive")
private Long productId;

@NotNull(message = "Quantity is required")
@Min(value = 1, message = "Minimum quantity is 1")
@Max(value = 100, message = "Maximum quantity is 100")
private Integer quantity;
}

Security Annotations

@PreAuthorize

Method-level access control

@GetMapping("/cart")
@PreAuthorize("hasRole('USER')") // Only users with USER role
public ResponseEntity<CartDto> getCart(Authentication auth) {
// Only accessible if user has USER role
}

Common expressions:

  • hasRole('ADMIN') - User has ADMIN role
  • hasAnyRole('USER', 'ADMIN') - User has any of these roles
  • isAuthenticated() - User is logged in
  • permitAll() - Anyone can access

Transaction Annotations

@Transactional

Marks method as transactional

@Service
@Transactional // All methods in this class are transactional
public class CartServiceImpl implements CartService {

@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public CartDto addToCart(Long userId, AddToCartRequest request) {
// If exception occurs, all database changes are rolled back
}
}

Attributes:

  • readOnly = true - Optimization for read operations
  • isolation - Transaction isolation level
  • rollbackFor - Which exceptions trigger rollback
  • propagation - How transactions nest

Socratic Questions

Q1: What's the difference between @Component, @Service, and @Repository?

A: Technically same (all create beans), but semantically different:

  • @Component - Generic
  • @Service - Business logic layer
  • @Repository - Data access layer (with exception translation)

Q2: Why use @RestController instead of @Controller?

A: @RestController = @Controller + @ResponseBody. Automatically converts return values to JSON. Without @ResponseBody, Spring looks for a view (like JSP).

Q3: What happens if @Valid validation fails?

A: Spring throws MethodArgumentNotValidException, caught by GlobalExceptionHandler, returns 400 Bad Request.


Bean Lifecycle

What is a Bean?

A bean is an object managed by Spring's IoC (Inversion of Control) container.

Lifecycle Stages

  1. Instantiation: Spring creates bean instance
  2. Populate Properties: Dependencies injected
  3. Bean Name Aware: If implements BeanNameAware
  4. Bean Factory Aware: If implements BeanFactoryAware
  5. Pre-Initialization: @PostConstruct methods run
  6. Initialization: After properties set
  7. Post-Initialization: Bean ready to use
  8. Destruction: @PreDestroy methods run

Example in Your Code

@Entity
@Table(name = "carts")
public class Cart implements Serializable {

@PrePersist // Before saving to database
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}

@PreUpdate // Before updating in database
protected void onUpdate() {
updatedAt = LocalDateTime.now();
calculateTotalPrice();
}
}

Bean Scopes

Singleton (Default)

@Service  // One instance for entire application
public class CartServiceImpl implements CartService {
}

Prototype

@Component
@Scope("prototype") // New instance each time requested
public class ReportGenerator {
}

Request/Session (Web applications)

@Component
@Scope("request") // New instance per HTTP request
public class UserSession {
}

Socratic Question

Q: Why is singleton scope default?

A: Most Spring beans are stateless services. Creating one instance and reusing it is efficient. They don't hold user-specific data.


Spring Data JPA

What is JPA?

JPA (Java Persistence API) is a specification for ORM (Object-Relational Mapping).

ORM: Maps Java objects to database tables automatically.

The Magic: Repository Pattern

public interface CartRepository extends JpaRepository<Cart, Long> {
Optional<Cart> findByUserId(Long userId);
}

No implementation needed! Spring Data JPA generates it.

How It Works

  1. Spring scans interfaces extending JpaRepository
  2. Creates proxy implementation at runtime
  3. Parses method names to generate SQL queries

Method Name Parsing

// findBy + PropertyName
Optional<Cart> findByUserId(Long userId);
// SQL: SELECT * FROM carts WHERE user_id = ?

// findBy + Property + And + AnotherProperty
List<Product> findByCategoryAndPriceGreaterThan(String category, BigDecimal price);
// SQL: SELECT * FROM products WHERE category = ? AND price > ?

// existsBy + Property
boolean existsByEmail(String email);
// SQL: SELECT COUNT(*) FROM users WHERE email = ?

Custom Queries

When method names aren't enough:

@Query("SELECT c FROM Cart c JOIN FETCH c.items WHERE c.user.id = :userId")
Optional<Cart> findByUserIdWithItems(@Param("userId") Long userId);

@Query: Write custom JPQL (Java Persistence Query Language) JOIN FETCH: Eagerly load items to avoid N+1 problem

Entity Relationships

@OneToOne

One user has one cart

@Entity
public class User {
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private Cart cart;
}

@Entity
public class Cart {
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}

@OneToMany / @ManyToOne

One cart has many items

@Entity
public class Cart {
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartItem> items = new ArrayList<>();
}

@Entity
public class CartItem {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cart_id")
private Cart cart;
}

Key Concepts:

  • mappedBy: The other side owns the relationship
  • cascade: Operations cascade to child entities
  • orphanRemoval: Delete child if removed from parent's collection
  • fetch = LAZY: Load only when accessed (performance optimization)

Socratic Questions

Q1: Why use Optional<Cart> as return type?

A: Explicitly handles "not found" case. Forces caller to check if present, avoiding NullPointerException.

Q2: What's the N+1 problem?

A: If you load Cart without JOIN FETCH, then access items, JPA makes 1 query for cart + N queries for each item. JOIN FETCH loads everything in one query.

Q3: What happens if you don't use @Transactional with JPA operations?

A: LazyInitializationException when accessing lazy-loaded relationships outside transaction scope.


Spring Security

Authentication vs Authorization

Authentication: "Who are you?" - Verifying identity

Authorization: "What can you do?" - Checking permissions

Security Filter Chain

HTTP Request → Filter Chain → Controller

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll() // Anyone
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Admin only
.requestMatchers("/api/cart/**").hasAnyRole("USER", "ADMIN") // Users
.anyRequest().authenticated() // Everything else needs login
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

JWT Filter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
// 1. Extract JWT from Authorization header
String token = extractToken(request);

// 2. Validate token
if (token != null && jwtUtil.validateToken(token)) {
// 3. Extract username from token
String username = jwtUtil.extractUsername(token);

// 4. Load user details
UserDetails userDetails = userService.loadUserByUsername(username);

// 5. Create authentication object
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);

// 6. Set in Security Context
SecurityContextHolder.getContext().setAuthentication(authentication);
}

// 7. Continue filter chain
filterChain.doFilter(request, response);
}
}

UserDetails Interface

@Entity
public class User implements UserDetails {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
.collect(Collectors.toList());
}

@Override
public String getUsername() {
return email; // We use email as username
}

@Override
public boolean isEnabled() {
return emailVerified && !accountLocked;
}
}

Why implement UserDetails?

Spring Security needs a standard way to:

  • Get username
  • Get password
  • Get authorities (roles)
  • Check if account is enabled/locked

Password Encoding

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

BCrypt: One-way hashing algorithm

  • Can't decrypt
  • Same password → different hash each time (salt)
  • Slow by design (prevents brute force)

Socratic Questions

Q1: Why use JWT instead of sessions?

A: Stateless authentication. Server doesn't store session data. JWT contains all info needed. Scales better for distributed systems.

Q2: Where is JWT stored in browser?

A: localStorage in this project. Alternative: httpOnly cookies (more secure against XSS).

Q3: What if JWT is stolen?

A: Attacker can impersonate user until token expires. Mitigation: short expiration time, refresh tokens, secure storage.


Transaction Management

What is a Transaction?

Group of database operations that must all succeed or all fail.

ACID Properties:

  • Atomicity: All or nothing
  • Consistency: Valid state before and after
  • Isolation: Concurrent transactions don't interfere
  • Durability: Committed changes persist

@Transactional in Action

@Service
@Transactional
public class CartServiceImpl implements CartService {

@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public CartDto addToCart(Long userId, AddToCartRequest request) {
// Start transaction

Product product = productRepository.findById(request.getProductId())
.orElseThrow(() -> new ProductNotFoundException(request.getProductId()));

Cart cart = getOrCreateCart(userId);

CartItem item = new CartItem();
item.setCart(cart);
item.setProduct(product);
item.setQuantity(request.getQuantity());

cart.addItem(item);
cart.calculateTotalPrice();

Cart savedCart = cartRepository.save(cart);

// Commit transaction - all database changes saved
// If exception thrown, rollback - no changes saved

return convertToDto(savedCart);
}
}

What Happens Behind the Scenes?

  1. Transaction begins when method is called
  2. Database connection obtained from pool
  3. All queries execute in same connection
  4. Method completes → COMMIT (save changes)
  5. Exception thrown → ROLLBACK (undo all changes)
  6. Connection returned to pool

Isolation Levels

Protect against concurrent access issues:

  • READ_UNCOMMITTED: Can read uncommitted changes (dirty reads)
  • READ_COMMITTED: Only read committed changes
  • REPEATABLE_READ: Same query returns same result in transaction
  • SERIALIZABLE: Complete isolation (slowest)

Common Issues

Issue 1: Calling transactional method from same class

@Service
public class CartServiceImpl {

@Transactional
public void methodA() {
methodB(); // ❌ Transaction not applied!
}

@Transactional
public void methodB() {
// This won't be transactional when called from methodA
}
}

Why? Spring uses proxies. Internal calls bypass proxy.

Solution: Inject self or restructure code.

Issue 2: Checked exceptions don't rollback

@Transactional  // Only rolls back on RuntimeException
public void saveData() throws IOException {
// If IOException thrown, transaction commits!
}

@Transactional(rollbackFor = Exception.class) // ✅ Rolls back on any exception
public void saveData() throws IOException {
// Now IOException also triggers rollback
}

Socratic Questions

Q1: Why use transactions?

A: Data consistency. Example: Adding to cart involves multiple tables (cart, cart_items). Either all succeed or none. Prevents partial updates.

Q2: What if we forget @Transactional?

A: Each database operation is its own transaction. If middle operation fails, earlier ones are committed. Inconsistent data!

Q3: Why is SERIALIZABLE slowest?

A: Locks more resources, allows less concurrency. Trades performance for maximum consistency.


Exception Handling

Global Exception Handler

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(CartNotFoundException.class)
public ResponseEntity<ErrorResponse> handleCartNotFound(CartNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
// Extract validation errors
Map<String, String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage
));

return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}

Custom Exceptions

public class CartNotFoundException extends RuntimeException {
public CartNotFoundException(Long userId) {
super("Cart not found for user ID: " + userId);
}
}

public class InsufficientStockException extends RuntimeException {
public InsufficientStockException(String productName, int available, int requested) {
super(String.format(
"Insufficient stock for %s. Available: %d, Requested: %d",
productName, available, requested
));
}
}

Why This Pattern?

Without Global Handler

@GetMapping("/cart")
public ResponseEntity<CartDto> getCart(Authentication auth) {
try {
CartDto cart = cartService.getCart(userId);
return ResponseEntity.ok(cart);
} catch (CartNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Something went wrong"));
}
}

Problems:

  • Duplicate code in every controller
  • Inconsistent error responses
  • Hard to maintain

With Global Handler

@GetMapping("/cart")
public ResponseEntity<CartDto> getCart(Authentication auth) {
CartDto cart = cartService.getCart(userId);
return ResponseEntity.ok(cart); // Clean!
}

Exception handling centralized in one place!

Socratic Questions

Q1: Why extend RuntimeException vs Exception?

A: RuntimeException is unchecked - don't need to declare in method signature. Exception is checked - must declare. For business logic exceptions, unchecked is more convenient.

Q2: When does GlobalExceptionHandler catch exceptions?

A: Only from controllers (@RestController). Service layer exceptions bubble up to controller, then caught by handler.

Q3: What if no handler matches the exception?

A: Spring's default handler returns 500 Internal Server Error with stack trace (in dev mode).


Summary

Key Takeaways

  1. Spring Boot = Convention + Automation: Focus on business logic, not infrastructure
  2. Dependency Injection = Loose Coupling: Objects don't create dependencies, Spring injects them
  3. Annotations = Metadata: Tell Spring what to do with classes/methods
  4. Beans = Managed Objects: Spring creates, configures, and manages lifecycle
  5. JPA = ORM: Objects ↔ Database tables automatically
  6. Security = Filter Chain: Requests pass through security checks
  7. Transactions = All-or-Nothing: Database consistency maintained
  8. Global Exception Handling = Centralized Error Management: Consistent error responses

Practice Exercises

1. Trace Dependency Injection

  • Pick a controller method
  • List all injected dependencies
  • Trace how Spring created each one

2. Create a New Feature

  • Add "Product Reviews" functionality
  • Entity, Repository, Service, Controller
  • Apply all concepts learned

3. Break and Fix

  • Remove @Transactional from a service method
  • Observe what happens
  • Understand why transactions matter

4. Security Experiment

  • Add a new admin endpoint
  • Test access with USER role (should fail)
  • Understand role-based access control

Next Steps

Now that you understand backend fundamentals, move to:

  • learning-map.md - Continue with the structured learning path
  • Architecture Deep Dive - See how layers work together (coming soon)
  • Backend Scenarios - Trace complete user flows (coming soon)

Questions? Test your understanding by implementing the practice exercises!

Each concept builds on the previous one. Take your time to understand each section before moving forward.