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
- What is Spring Boot?
- Dependency Injection (The Heart of Spring)
- Annotations Explained
- Bean Lifecycle
- Spring Data JPA
- Spring Security
- Transaction Management
- 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?
-
@SpringBootApplicationcombines three annotations:@Configuration- This class can define beans@EnableAutoConfiguration- Spring Boot's magic@ComponentScan- Look for components in this package
-
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
- Spring Container (ApplicationContext) creates and manages objects (beans)
- When CartServiceImpl is needed, Spring:
- Creates CartRepository instance
- Creates ProductRepository instance
- Injects them into CartServiceImpl constructor
- All managed by Spring - you just declare dependencies!
Three Types of Injection
1. Constructor Injection (Recommended ✅)
@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 rolehasAnyRole('USER', 'ADMIN')- User has any of these rolesisAuthenticated()- User is logged inpermitAll()- 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 operationsisolation- Transaction isolation levelrollbackFor- Which exceptions trigger rollbackpropagation- 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
- Instantiation: Spring creates bean instance
- Populate Properties: Dependencies injected
- Bean Name Aware: If implements BeanNameAware
- Bean Factory Aware: If implements BeanFactoryAware
- Pre-Initialization: @PostConstruct methods run
- Initialization: After properties set
- Post-Initialization: Bean ready to use
- 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
- Spring scans interfaces extending JpaRepository
- Creates proxy implementation at runtime
- 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?
- Transaction begins when method is called
- Database connection obtained from pool
- All queries execute in same connection
- Method completes → COMMIT (save changes)
- Exception thrown → ROLLBACK (undo all changes)
- 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
- Spring Boot = Convention + Automation: Focus on business logic, not infrastructure
- Dependency Injection = Loose Coupling: Objects don't create dependencies, Spring injects them
- Annotations = Metadata: Tell Spring what to do with classes/methods
- Beans = Managed Objects: Spring creates, configures, and manages lifecycle
- JPA = ORM: Objects ↔ Database tables automatically
- Security = Filter Chain: Requests pass through security checks
- Transactions = All-or-Nothing: Database consistency maintained
- 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.