Skip to main content

Backend Scenarios: Complete Request Flow Analysis

Introduction​

This guide traces complete HTTP request journeys through the backend. You'll see exactly what happens at each layer, from HTTP request arrival to database query to response.


Table of Contents​

  1. Scenario 1: User Login
  2. Scenario 2: Add Item to Cart
  3. Scenario 3: Get Cart with Items
  4. Scenario 4: Update Cart Item Quantity
  5. Scenario 5: Admin Create Product

Scenario 1: User Login​

User Story​

"As a user, I want to log in with my email and password to access my account"

HTTP Request​

POST /api/auth/login HTTP/1.1
Content-Type: application/json

{
"email": "user@automotive.com",
"password": "user123"
}

Journey Through the Stack​

Step 1: Request Arrives at Spring Boot​

Tomcat Server (Port 8080)
↓
Dispatcher Servlet (Spring's request router)
↓
Security Filter Chain

Security Filter Chain checks:

  • Is /api/auth/** in permitAll list? ✅ Yes
  • No authentication needed, continue to controller

Step 2: Controller Receives Request​

File: AuthController.java

@RestController
@RequestMapping("/api/auth")
public class AuthController {

@Autowired
private AuthService authService;

@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
// 1. @Valid triggers validation on LoginRequest
// - email must be valid email format
// - password must not be blank

// 2. If validation passes, call service
LoginResponse response = authService.authenticate(request);

// 3. Return response
return ResponseEntity.ok(response);
}
}

What happens here?

  1. Spring maps POST /api/auth/login to this method
  2. Converts JSON → LoginRequest object (Jackson library)
  3. Validates @NotBlank and @Email annotations
  4. Calls authService.authenticate()

Step 3: Service Layer - Authentication Logic​

File: AuthService.java

@Service
public class AuthService {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserService userService;

@Autowired
private JwtUtil jwtUtil;

public LoginResponse authenticate(LoginRequest loginRequest) {
// 1. Create authentication token
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword()
);

// 2. Attempt authentication
Authentication authentication =
authenticationManager.authenticate(authToken);
// ↓
// This triggers:
// - UserService.loadUserByUsername()
// - Password comparison (BCrypt)
// - Account status checks (locked, enabled)

// 3. Authentication successful - extract user
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
User user = (User) userDetails;

// 4. Reset failed login attempts
userService.resetFailedLoginAttempts(loginRequest.getEmail());

// 5. Generate JWT token
String token = jwtUtil.generateToken(userDetails);

// 6. Build response with user data + token
return new LoginResponse(
token,
user.getId(),
user.getEmail(),
user.getFirstName(),
user.getLastName(),
user.getRoles()
);
}
}

Step 4: Authentication Manager Deep Dive​

When authenticationManager.authenticate() is called:

1. Spring Security invokes UserService:

@Service
public class UserService implements UserDetailsService {

@Autowired
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) {
// Database query: SELECT * FROM users WHERE email = ?
return userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}

2. Spring Security compares passwords:

// Password from request: "user123"
// Password from database: "$2a$10$..." (BCrypt hash)

boolean matches = passwordEncoder.matches(
"user123", // Plain text
"$2a$10$..." // BCrypt hash from DB
);

// BCrypt hashes "user123" and compares with stored hash
// If match → authentication successful

3. Spring Security checks account status:

@Entity
public class User implements UserDetails {

@Override
public boolean isAccountNonLocked() {
return !accountLocked; // Check if locked
}

@Override
public boolean isEnabled() {
return emailVerified && !accountLocked; // Check if enabled
}
}

Step 5: JWT Token Generation​

File: JwtUtil.java

public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities());

return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername()) // email
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}

JWT Structure:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyQGF1dG9tb3RpdmUuY29tIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ.signature

Header.Payload.Signature

Payload contains:

  • sub: user@automotive.com (username)
  • roles: ["ROLE_USER"]
  • iat: Issued at timestamp
  • exp: Expiration timestamp

Step 6: Response Sent to Client​

HTTP Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"id": 2,
"email": "user@automotive.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["USER"]
}

Database Queries Executed​

  1. SELECT * FROM users WHERE email = 'user@automotive.com' (load user)
  2. UPDATE users SET failed_login_attempts = 0 WHERE email = 'user@automotive.com' (reset attempts)

Complete Flow Diagram​

[Browser]
↓ POST /api/auth/login
↓ {email, password}
[Tomcat Server:8080]
↓
[Security Filter Chain] → permitAll ✓
↓
[AuthController]
↓ @PostMapping("/login")
↓ Validate @Valid
[AuthService]
↓ authenticate()
[AuthenticationManager]
↓
[UserService] → loadUserByUsername()
↓
[UserRepository] → findByEmail()
↓
[H2 Database] ← SELECT * FROM users
↑
[UserRepository] → User entity
↑
[PasswordEncoder] → verify password
↑ matches ✓
[JwtUtil] → generateToken()
↑
[AuthService] → LoginResponse
↑
[AuthController] → ResponseEntity
↑
[Browser] ← HTTP 200 + JSON
↑
localStorage.setItem('token', ...)

Socratic Questions​

Q1: What happens if password is wrong? A: authenticationManager.authenticate() throws BadCredentialsException. Caught by AuthService, which increments failed_login_attempts. After 5 failures, account locks.

Q2: Why use BCrypt instead of storing plain password? A: Security! If database is compromised, passwords aren't exposed. BCrypt is one-way (can't decrypt). Rainbow table resistant due to salt.

Q3: What if JWT token is modified by attacker? A: Signature verification fails. JWT signature uses secret key. Without key, can't create valid signature. Modified token rejected.


Scenario 2: Add Item to Cart​

User Story​

"As a logged-in user, I want to add a product to my shopping cart"

HTTP Request​

POST /api/cart/items HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json

{
"productId": 5,
"quantity": 2
}

Journey Through the Stack​

Step 1: Security Filter Processes JWT​

File: JwtAuthenticationFilter.java

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

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

String token = null;
if (header != null && header.startsWith("Bearer ")) {
token = header.substring(7); // Remove "Bearer " prefix
}

// 2. Validate token
if (token != null && jwtUtil.validateToken(token)) {
// 3. Extract username
String username = jwtUtil.extractUsername(token);
// "user@automotive.com"

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

// 5. Create authentication object
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, // Principal
null, // Credentials (not needed)
userDetails.getAuthorities() // Roles
);

// 6. Set in Security Context (thread-local storage)
SecurityContextHolder.getContext().setAuthentication(authentication);
// Now this request is authenticated!
}

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

What happens?

  • JWT token extracted and validated
  • User loaded from database (or cache)
  • Authentication stored in SecurityContext
  • Request proceeds to controller

Step 2: Controller Receives Request​

File: CartController.java

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

@Autowired
private CartService cartService;

@PostMapping("/items")
@PreAuthorize("hasRole('USER')") // Check role
public ResponseEntity<CartDto> addToCart(
@RequestBody @Valid AddToCartRequest request,
Authentication authentication) {

// 1. Get user ID from authentication
Long userId = getUserIdFromAuthentication(authentication);
// User entity from SecurityContext

// 2. Log the request
log.info("Add to cart: user={}, product={}, quantity={}",
userId, request.getProductId(), request.getQuantity());

// 3. Call service
CartDto updatedCart = cartService.addToCart(userId, request);

// 4. Return response
return ResponseEntity.status(HttpStatus.CREATED).body(updatedCart);
}

private Long getUserIdFromAuthentication(Authentication authentication) {
User user = (User) authentication.getPrincipal();
return user.getId();
}
}

@PreAuthorize Check:

  • Before method executes, Spring Security checks role
  • User must have "ROLE_USER" authority
  • If not, throws AccessDeniedException → 403 Forbidden

Step 3: Service Layer - Business Logic​

File: CartServiceImpl.java

@Service
@Transactional // Start transaction
public class CartServiceImpl implements CartService {

@Autowired
private CartRepository cartRepository;

@Autowired
private ProductRepository productRepository;

@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public CartDto addToCart(Long userId, AddToCartRequest request) {
// Transaction starts here

// 1. Find product (validate exists and active)
Product product = productRepository
.findByIdAndIsActiveTrue(request.getProductId())
.orElseThrow(() -> new ProductNotFoundException(request.getProductId()));

// 2. Validate stock availability
if (product.getStockQuantity() < request.getQuantity()) {
throw new InsufficientStockException(
product.getName(),
product.getStockQuantity(),
request.getQuantity()
);
}

// 3. Get or create cart for user
Cart cart = getOrCreateCart(userId);

// 4. Check if product already in cart
Optional<CartItem> existingItem = cartItemRepository
.findByCartIdAndProductId(cart.getId(), request.getProductId());

if (existingItem.isPresent()) {
// Update quantity
CartItem item = existingItem.get();
int newQuantity = item.getQuantity() + request.getQuantity();

// Validate new quantity doesn't exceed stock
if (product.getStockQuantity() < newQuantity) {
throw new InsufficientStockException(...);
}

item.setQuantity(newQuantity);
} else {
// Create new cart item
CartItem newItem = new CartItem();
newItem.setCart(cart);
newItem.setProduct(product);
newItem.setQuantity(request.getQuantity());
newItem.setPrice(product.getPrice());

cart.addItem(newItem); // Adds to list and sets bidirectional relationship
}

// 5. Recalculate cart total
cart.calculateTotalPrice();
// total = sum of (item.price * item.quantity) for all items

// 6. Save cart (cascades to cart items)
Cart savedCart = cartRepository.save(cart);

// Transaction commits here (if no exception)
// All database changes are permanently saved

// 7. Convert to DTO and return
return convertToDto(savedCart);
}

private Cart getOrCreateCart(Long userId) {
return cartRepository.findByUserIdWithItems(userId)
.orElseGet(() -> {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
return cartRepository.save(new Cart(user));
});
}
}

Step 4: Repository Layer - Database Access​

File: CartRepository.java

public interface CartRepository extends JpaRepository<Cart, Long> {

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

SQL Generated:

SELECT
c.id, c.user_id, c.total_price, c.created_at, c.updated_at,
ci.id, ci.product_id, ci.quantity, ci.price
FROM carts c
LEFT JOIN cart_items ci ON c.id = ci.cart_id
WHERE c.user_id = ?

JOIN FETCH: Loads cart and all items in one query (avoids N+1 problem)

Step 5: Entity Relationships in Action​

File: Cart.java

@Entity
public class Cart {

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

public void addItem(CartItem item) {
items.add(item);
item.setCart(this); // Bidirectional relationship
calculateTotalPrice();
}

@PreUpdate // Called automatically before UPDATE
protected void onUpdate() {
updatedAt = LocalDateTime.now();
calculateTotalPrice();
}

public void calculateTotalPrice() {
this.totalPrice = items.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}

Cascade.ALL: When cart is saved, cart items automatically saved orphanRemoval: When item removed from list, deleted from database

Step 6: Response Conversion​

private CartDto convertToDto(Cart cart) {
CartDto dto = new CartDto();
dto.setId(cart.getId());
dto.setUserId(cart.getUser().getId());

// Convert each cart item
List<CartItemDto> itemDtos = cart.getItems().stream()
.map(this::convertToCartItemDto)
.collect(Collectors.toList());

dto.setItems(itemDtos);
dto.setTotalPrice(cart.getTotalPrice());
dto.setItemCount(cart.getItemCount());

return dto;
}

Why DTO instead of Entity?

  • Hide internal structure
  • Prevent infinite recursion (bidirectional relationships)
  • Only send needed data
  • Avoid lazy loading issues

Step 7: Response Sent​

HTTP Response:

HTTP/1.1 201 Created
Content-Type: application/json

{
"id": 3,
"userId": 2,
"items": [
{
"id": 7,
"productId": 5,
"productName": "Engine Oil Filter",
"quantity": 2,
"unitPrice": 12.99,
"subtotal": 25.98
}
],
"totalPrice": 25.98,
"itemCount": 1
}

Database Queries Executed​

  1. SELECT * FROM products WHERE id = 5 AND is_active = true
  2. SELECT c.*, ci.* FROM carts c LEFT JOIN cart_items ci ON ... WHERE c.user_id = 2
  3. INSERT INTO cart_items (cart_id, product_id, quantity, price) VALUES (?, ?, ?, ?)
  4. UPDATE carts SET total_price = ?, updated_at = ? WHERE id = ?

Transaction Flow​

Transaction Start
↓
Find Product (READ)
↓
Get Cart (READ)
↓
Create CartItem (INSERT)
↓
Update Cart.items (relationship)
↓
Recalculate total (UPDATE)
↓
Save Cart (triggers cascade to CartItem)
↓
Transaction Commit ✓
↓
All changes permanent

If exception occurs: Transaction ROLLBACK, all changes undone

Complete Flow Diagram​

[Browser with JWT token]
↓ POST /api/cart/items
↓ Authorization: Bearer <token>
[JwtAuthenticationFilter]
↓ Extract token
↓ Validate signature
↓ Load user
↓ Set SecurityContext
[PreAuthorize] → hasRole('USER') ✓
↓
[CartController]
↓ @PostMapping("/items")
↓ Extract userId
↓ Validate request
[CartService] @Transactional START
↓ addToCart()
↓
[ProductRepository] → findById()
↓ Database: SELECT * FROM products
↑
[Validate] stock availability ✓
↓
[CartRepository] → findByUserIdWithItems()
↓ Database: SELECT carts + cart_items
↑
[Create CartItem]
↓ Set relationships
↓
[Cart] → addItem(), calculateTotal()
↓
[CartRepository] → save()
↓ Database: INSERT cart_items
↓ Database: UPDATE carts
@Transactional COMMIT ✓
↑
[Convert to DTO]
↑
[CartController] → Response
↑
[Browser] ← HTTP 201 + CartDto JSON

Socratic Questions​

Q1: Why use @Transactional here? A: Multiple database operations must succeed together. If stock check passes but save fails, we don't want partial data. Transaction ensures atomicity.

Q2: What if two users add last item simultaneously? A: Isolation.REPEATABLE_READ prevents this. First transaction locks the product row. Second transaction waits or gets stale data, then fails validation on commit.

Q3: Why convert to DTO instead of returning Cart entity directly? A: Security (hide sensitive fields), prevent infinite loops (bidirectional relationships), avoid lazy loading exceptions, and API stability (internal changes don't affect API).


Scenario 3: Get Cart with Items​

User Story​

"As a user, I want to view my shopping cart"

HTTP Request​

GET /api/cart HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Simplified Flow (Building on Previous Knowledge)​

1. Security Filter → Authenticates request (same as Scenario 2)​

2. Controller​

@GetMapping
@PreAuthorize("hasRole('USER')")
public ResponseEntity<CartDto> getCart(Authentication authentication) {
Long userId = getUserIdFromAuthentication(authentication);
CartDto cart = cartService.getCart(userId);
return ResponseEntity.ok(cart);
}

3. Service Layer​

@Override
@Transactional(readOnly = true) // Optimization for read operations
public CartDto getCart(Long userId) {
Optional<Cart> cartOptional = cartRepository.findByUserIdWithItems(userId);

Cart cart;
if (cartOptional.isPresent()) {
cart = cartOptional.get();
} else {
// No cart exists, create empty one
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
cart = cartRepository.save(new Cart(user));
}

return convertToDto(cart);
}

@Transactional(readOnly = true):

  • Database can optimize (no locks needed)
  • Prevents accidental writes
  • Better performance

4. Repository​

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

Single Query:

SELECT c.*, ci.*
FROM carts c
LEFT JOIN cart_items ci ON c.id = ci.cart_id
WHERE c.user_id = 2

Without JOIN FETCH (N+1 problem):

SELECT * FROM carts WHERE user_id = 2         -- 1 query
SELECT * FROM cart_items WHERE cart_id = 3 -- N queries (one per cart)

Response​

{
"id": 3,
"userId": 2,
"items": [
{
"id": 7,
"productId": 5,
"productName": "Engine Oil Filter",
"quantity": 2,
"unitPrice": 12.99,
"subtotal": 25.98
},
{
"id": 8,
"productId": 12,
"productName": "Brake Pads",
"quantity": 1,
"unitPrice": 45.00,
"subtotal": 45.00
}
],
"totalPrice": 70.98,
"itemCount": 2
}

Scenario 4: Update Cart Item Quantity​

User Story​

"As a user, I want to change the quantity of an item in my cart"

HTTP Request​

PUT /api/cart/items/7 HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json

{
"quantity": 5
}

Key Points​

Controller​

@PutMapping("/items/{itemId}")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<CartDto> updateCartItem(
@PathVariable Long itemId, // Extracted from URL
@Valid @RequestBody UpdateCartItemRequest request,
Authentication authentication) {

Long userId = getUserIdFromAuthentication(authentication);
CartDto updatedCart = cartService.updateCartItem(userId, itemId, request);
return ResponseEntity.ok(updatedCart);
}

@PathVariable: Extracts itemId from URL /items/7 → itemId = 7

Service Layer​

@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public CartDto updateCartItem(Long userId, Long itemId, UpdateCartItemRequest request) {
// 1. Get cart with items
Cart cart = cartRepository.findByUserIdWithItems(userId)
.orElseThrow(() -> new CartNotFoundException(userId));

// 2. Find specific item in cart
CartItem item = cart.getItems().stream()
.filter(ci -> ci.getId().equals(itemId))
.findFirst()
.orElseThrow(() -> new CartItemNotFoundException("Item not found"));

// 3. Validate stock availability
Product product = item.getProduct();
if (product.getStockQuantity() < request.getQuantity()) {
throw new InsufficientStockException(...);
}

// 4. Update quantity
item.setQuantity(request.getQuantity());

// 5. Recalculate cart total
cart.calculateTotalPrice();

// 6. Save and return
Cart savedCart = cartRepository.save(cart);
return convertToDto(savedCart);
}

Database Queries​

  1. SELECT c.*, ci.* FROM carts c LEFT JOIN cart_items ci... WHERE c.user_id = ?
  2. SELECT * FROM products WHERE id = ? (validate stock)
  3. UPDATE cart_items SET quantity = ? WHERE id = ?
  4. UPDATE carts SET total_price = ?, updated_at = ? WHERE id = ?

Scenario 5: Admin Create Product​

User Story​

"As an admin, I want to create a new product"

HTTP Request​

POST /api/admin/products HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... (admin token)
Content-Type: application/json

{
"name": "Spark Plug Set",
"brand": "NGK",
"partNumber": "SP-4589",
"category": "Engine Parts",
"price": 29.99,
"stockQuantity": 50,
"description": "High-performance spark plugs",
"isActive": true
}

Key Difference: Role-Based Access​

Security Configuration​

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Only ADMIN
.requestMatchers("/api/cart/**").hasAnyRole("USER", "ADMIN") // USER or ADMIN
);
return http.build();
}

Controller​

@RestController
@RequestMapping("/api/admin")
public class ProductController {

@PostMapping("/products")
@PreAuthorize("hasRole('ADMIN')") // Double-check role
public ResponseEntity<ProductResponseDTO> createProduct(
@Valid @RequestBody ProductRequestDTO request) {

// If user doesn't have ADMIN role:
// AccessDeniedException → 403 Forbidden

ProductResponseDTO created = productService.createProduct(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
}

Service Layer​

@Service
@Transactional
public class ProductService {

public ProductResponseDTO createProduct(ProductRequestDTO request) {
// 1. Validate part number uniqueness
if (productRepository.existsByPartNumber(request.getPartNumber())) {
throw new DuplicatePartNumberException(request.getPartNumber());
}

// 2. Create entity from DTO
Product product = new Product();
product.setName(request.getName());
product.setBrand(request.getBrand());
product.setPartNumber(request.getPartNumber());
product.setCategory(request.getCategory());
product.setPrice(request.getPrice());
product.setStockQuantity(request.getStockQuantity());
product.setDescription(request.getDescription());
product.setIsActive(request.getIsActive());

// 3. Save to database
Product saved = productRepository.save(product);

// 4. Convert to response DTO
return convertToResponseDTO(saved);
}
}

Access Control Flow​

[Admin with JWT token] → POST /api/admin/products
↓
[JwtAuthenticationFilter]
↓ Extract user from token
↓ Roles: ["ROLE_ADMIN"]
[SecurityFilterChain]
↓ Check: /api/admin/** requires ADMIN role
↓ User has ADMIN? ✓ Allow
[ProductController]
↓ @PreAuthorize("hasRole('ADMIN')")
↓ Double-check role ✓
[ProductService]
↓ Business logic
[Database] ← INSERT product
↑
[Response] → HTTP 201 Created

---

[Regular User with JWT token] → POST /api/admin/products
↓
[JwtAuthenticationFilter]
↓ Extract user from token
↓ Roles: ["ROLE_USER"]
[SecurityFilterChain]
↓ Check: /api/admin/** requires ADMIN role
↓ User has ADMIN? ✗ Deny
[AccessDeniedException]
↓
[CustomAccessDeniedHandler]
↓
[Response] → HTTP 403 Forbidden

Summary​

Common Patterns Across Scenarios​

  1. Security First: JWT validated, roles checked
  2. Controller: Receives request, delegates to service
  3. Service: Business logic, transactions
  4. Repository: Database access
  5. DTO Conversion: Entity ↔ DTO for API communication

Request Flow Layers​

HTTP Request
↓
Security Filters (JWT, roles)
↓
Controller (REST endpoints)
↓
Service (business logic, @Transactional)
↓
Repository (database access)
↓
Database (H2)
↑
Entity → DTO conversion
↑
HTTP Response

Practice Exercises​

  1. Trace Remove Item Flow:

    • Write out complete flow for DELETE /api/cart/items/7
    • Include all layers, queries, transaction behavior
  2. Add Logging:

    • Add log statements at each layer
    • Run request, observe logs in console
    • Understand timing and flow
  3. Break Security:

    • Remove @PreAuthorize from cart endpoint
    • Try to access as unauthenticated user
    • Observe what fails and why
  4. Examine Database:

    • Open H2 console during request
    • See data before and after
    • Understand how entities map to tables

Next Steps​

Now that you understand backend request flows:

  • Review frontend-fundamentals.md for React concepts
  • Study frontend-scenarios.md for complete UI flows
  • See how frontend and backend connect

Questions? Test your understanding by tracing requests through the code!