Skip to main content

Architecture Deep Dive - E-Commerce Project

A Complete Guide to N-Tier Layered Architecture Understanding how your automotive e-commerce application is structured


Table of Contents

  1. Introduction to N-Tier Architecture
  2. Your Project's Architecture
  3. Layer 1: Presentation Layer (Frontend)
  4. Layer 2: API Layer (Controllers)
  5. Layer 3: Business Logic Layer (Services)
  6. Layer 4: Data Access Layer (Repositories)
  7. Layer 5: Database Layer
  8. Cross-Cutting Concerns
  9. Request Flow Examples
  10. Architecture Benefits and Trade-offs

Introduction to N-Tier Architecture

What is N-Tier Architecture?

N-Tier (also called layered architecture) is a software architecture pattern that separates an application into distinct layers, each with specific responsibilities. Each layer:

  • Has a single, well-defined responsibility
  • Communicates only with adjacent layers
  • Can be developed, tested, and maintained independently

Think of it like a building:

  • Foundation (Database) - Stores data permanently
  • Structure (Data Access) - Provides access to the foundation
  • Interior (Business Logic) - Where the work happens
  • Interface (API) - Connects to the outside
  • Facade (UI) - What users see and interact with

Why use N-Tier Architecture?

Separation of Concerns - Each layer does one thing well ✅ Maintainability - Easy to locate and fix bugs ✅ Testability - Each layer can be tested independently ✅ Scalability - Layers can scale independently ✅ Reusability - Business logic can be used by different UIs ✅ Team Collaboration - Different teams can work on different layers


Your Project's Architecture

Your e-commerce application follows a 5-tier layered architecture:

┌─────────────────────────────────────────────────────┐
│ Layer 1: Presentation (Frontend) │
│ React Components + UI │
│ Port: 3000 | Tech: React 18, Axios │
└─────────────────────────────────────────────────────┘
↕ HTTP/JSON
┌─────────────────────────────────────────────────────┐
│ Layer 2: API (Controllers) │
│ @RestController Classes │
│ Port: 8080 | Tech: Spring MVC, REST │
└─────────────────────────────────────────────────────┘
↕ Method Calls
┌─────────────────────────────────────────────────────┐
│ Layer 3: Business Logic (Services) │
│ @Service Classes + Interfaces │
│ Tech: Spring Service, Transactions │
└─────────────────────────────────────────────────────┘
↕ Repository Calls
┌─────────────────────────────────────────────────────┐
│ Layer 4: Data Access (Repositories) │
│ @Repository Interfaces extending JPA │
│ Tech: Spring Data JPA │
└─────────────────────────────────────────────────────┘
↕ SQL Queries
┌─────────────────────────────────────────────────────┐
│ Layer 5: Database (H2) │
│ Tables: users, cart, products, etc. │
│ Tech: H2 File-based Database │
└─────────────────────────────────────────────────────┘

Key Rule: Each layer only communicates with the layer directly below it!

Never do this:

  • Controller calling Repository directly
  • Frontend calling Database directly
  • Service calling another Controller

Always do this:

  • Controller → Service → Repository → Database
  • Each layer depends on the layer below through interfaces

Layer 1: Presentation Layer (Frontend)

Location: frontend/src/

Responsibility: User interface and user experience

Technologies:

  • React 18 (UI library)
  • Axios (HTTP client)
  • React Router DOM (routing)
  • React Context API (state management)

Key Components:

frontend/src/
├── components/
│ ├── Header.js # Navigation, user info, cart count
│ ├── Login.js # Authentication form
│ ├── UserDashboard.js # User product browsing
│ ├── AdminDashboard.js # Admin product management
│ └── Dashboard.js # Main dashboard router
├── context/
│ └── AuthContext.js # Global authentication state
├── services/
│ └── api.js # HTTP client with interceptors
└── App.js # Root component with routing

What this layer does:

  1. Renders UI - Displays data to users
  2. Handles user input - Forms, buttons, interactions
  3. Makes API calls - Sends requests to backend
  4. Manages local state - UI state (loading, errors)
  5. Client-side routing - Navigation between pages

What this layer does NOT do:

❌ Business logic (validation, calculations) ❌ Database access ❌ Authentication (only stores token) ❌ Server-side operations

Example: Login Component

// Login.js (simplified)
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login } = useAuth();

const handleSubmit = async (e) => {
e.preventDefault();

// 1. Gather user input
const credentials = { email, password };

try {
// 2. Send to API layer (Layer 2)
const response = await axios.post('/api/auth/login', credentials);

// 3. Store token and update state
login(response.data.token, response.data);

// 4. Navigate to dashboard
navigate('/dashboard');
} catch (error) {
// 5. Handle errors (display to user)
toast.error('Login failed');
}
};

// 6. Render UI
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
<button type="submit">Login</button>
</form>
);
}

Layer Communication:

User clicks Login button

Login component handleSubmit()

axios.post('/api/auth/login', credentials)

HTTP POST request to Layer 2 (API)

Response received

Update UI state and navigate

Why separate frontend?

  • Different technology stack (React vs Java)
  • Can be developed by different team
  • Can be deployed independently
  • Multiple frontends possible (web, mobile, desktop)

Layer 2: API Layer (Controllers)

Location: backend/src/main/java/com/automotive/ecommerce/business/controller/

Responsibility: HTTP request/response handling

Technologies:

  • Spring MVC
  • Spring Security
  • JSON serialization/deserialization

Key Controllers:

controller/
├── AuthController.java # /api/auth/* - Authentication
├── CartController.java # /api/cart/* - Shopping cart
├── ProductController.java # /api/admin/products/* - Admin products
└── PublicProductController.java # /api/products/* - Public products

What this layer does:

  1. Receives HTTP requests - GET, POST, PUT, DELETE
  2. Validates input - @Valid annotations
  3. Extracts authentication - From JWT tokens
  4. Calls service layer - Delegates business logic
  5. Formats responses - Convert to JSON
  6. Handles HTTP status codes - 200, 404, 401, etc.

What this layer does NOT do:

❌ Business logic ❌ Database queries ❌ Complex calculations ❌ Transaction management

Example: CartController

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

private final CartService cartService; // Dependency on Layer 3

@Autowired
public CartController(CartService cartService) {
this.cartService = cartService;
}

// GET /api/cart
@GetMapping
@PreAuthorize("hasRole('USER')") // Security check
public ResponseEntity<CartDto> getCart(Authentication authentication) {
// 1. Extract user ID from authentication
Long userId = getUserIdFromAuthentication(authentication);

// 2. Call service layer (Layer 3)
CartDto cart = cartService.getCart(userId);

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

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

// 1. Extract user ID
Long userId = getUserIdFromAuthentication(authentication);

// 2. Delegate to service (Layer 3)
CartDto cart = cartService.addToCart(userId, request);

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

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

Layer Communication:

HTTP POST /api/cart/items arrives

Spring routes to CartController.addToCart()

Spring Security validates JWT token

@Valid validates AddToCartRequest

Extract userId from Authentication

Call cartService.addToCart(userId, request) ← Layer 3

Receive CartDto from service

Convert to JSON and return HTTP 200

Controller Responsibilities Breakdown:

ConcernController's Role
HTTP✅ Handles requests/responses, status codes
Validation✅ Input validation with @Valid
Security✅ @PreAuthorize checks
Data Format✅ JSON serialization/deserialization
Business Logic❌ Delegates to service layer
Database❌ No direct database access
Transactions❌ Managed by service layer

Layer 3: Business Logic Layer (Services)

Location: backend/src/main/java/com/automotive/ecommerce/business/service/

Responsibility: Core business logic and rules

Technologies:

  • Spring Service
  • Spring Transactions (@Transactional)
  • Business rule validation

Key Services:

service/
├── CartService.java # Interface
├── CartServiceImpl.java # Implementation
├── AuthService.java
├── ProductService.java
├── UserService.java
└── CartAuditService.java

What this layer does:

  1. Implements business rules - Stock validation, pricing logic
  2. Coordinates between repositories - Multiple database operations
  3. Manages transactions - Ensures data consistency
  4. Performs calculations - Cart totals, discounts
  5. Converts entities to DTOs - Data transformation
  6. Orchestrates complex operations - Multi-step workflows

What this layer does NOT do:

❌ HTTP handling ❌ JSON serialization ❌ Direct SQL queries ❌ UI concerns

Example: CartServiceImpl

// CartServiceImpl.java
@Service
@Transactional // All methods run in transactions
public class CartServiceImpl implements CartService {

// Dependencies on Layer 4 (Repositories)
private final CartRepository cartRepository;
private final CartItemRepository cartItemRepository;
private final ProductRepository productRepository;
private final UserRepository userRepository;

@Autowired
public CartServiceImpl(CartRepository cartRepository,
CartItemRepository cartItemRepository,
ProductRepository productRepository,
UserRepository userRepository) {
this.cartRepository = cartRepository;
this.cartItemRepository = cartItemRepository;
this.productRepository = productRepository;
this.userRepository = userRepository;
}

@Override
@Transactional(readOnly = true) // Optimized for read
public CartDto getCart(Long userId) {
// 1. Fetch cart from repository (Layer 4)
Optional<Cart> cartOptional = cartRepository.findByUserIdWithItems(userId);

Cart cart;
if (cartOptional.isPresent()) {
cart = cartOptional.get();
} else {
// 2. Business logic: Create new cart if doesn't exist
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found"));
cart = createNewCart(user);
}

// 3. Convert entity to DTO
return convertToDto(cart);
}

@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public CartDto addToCart(Long userId, AddToCartRequest request) {
// 1. Fetch product (Layer 4)
Product product = productRepository.findByIdAndIsActiveTrue(request.getProductId())
.orElseThrow(() -> new ProductNotFoundException(request.getProductId()));

// 2. Business rule: Validate stock availability
validateStock(product, request.getQuantity());

// 3. Business logic: Get or create cart
Cart cart = getOrCreateCart(userId);

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

if (existingItem.isPresent()) {
// Update existing item quantity
CartItem item = existingItem.get();
int newQuantity = item.getQuantity() + request.getQuantity();
validateStock(product, newQuantity); // Re-validate
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);
}

// 5. Business calculation: Recalculate cart total
calculateCartTotal(cart);

// 6. Persist changes (Layer 4)
Cart savedCart = cartRepository.save(cart);

// 7. Convert to DTO for Layer 2
return convertToDto(savedCart);
}

// Private helper methods

private void validateStock(Product product, int requestedQuantity) {
if (product.getStockQuantity() < requestedQuantity) {
throw new InsufficientStockException(
product.getName(),
product.getStockQuantity(),
requestedQuantity
);
}
}

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

private Cart createNewCart(User user) {
Cart cart = new Cart(user);
return cartRepository.save(cart);
}

private void calculateCartTotal(Cart cart) {
cart.calculateTotalPrice(); // Business calculation
}

private CartDto convertToDto(Cart cart) {
List<CartItemDto> itemDtos = cart.getItems().stream()
.map(this::convertToCartItemDto)
.collect(Collectors.toList());

CartDto dto = new CartDto();
dto.setId(cart.getId());
dto.setUserId(cart.getUser().getId());
dto.setItems(itemDtos);
dto.setTotalPrice(cart.getTotalPrice());
dto.setItemCount(cart.getItemCount());

return dto;
}

private CartItemDto convertToCartItemDto(CartItem item) {
Product product = item.getProduct();
BigDecimal subtotal = item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()));

CartItemDto dto = new CartItemDto();
dto.setId(item.getId());
dto.setProductId(product.getId());
dto.setProductName(product.getName());
dto.setQuantity(item.getQuantity());
dto.setUnitPrice(item.getPrice());
dto.setSubtotal(subtotal);

return dto;
}
}

Service Layer Responsibilities:

ConcernService's Role
Business Rules✅ Validates stock, prices, business constraints
Transactions✅ @Transactional ensures data consistency
Orchestration✅ Coordinates multiple repositories
Calculations✅ Cart totals, discounts, taxes
Data Conversion✅ Entity → DTO, DTO → Entity
Error Handling✅ Throws business exceptions
HTTP❌ No HTTP concerns
SQL❌ No direct SQL, uses repositories

Why @Transactional?

@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public CartDto addToCart(Long userId, AddToCartRequest request) {
// Multiple database operations:
// 1. Read product
// 2. Read cart
// 3. Read/update cart item
// 4. Update cart total
// 5. Save cart

// If ANY operation fails, ALL changes are rolled back!
// Database remains consistent.
}

Transaction Example:

START TRANSACTION
1. SELECT * FROM product WHERE id = ?
2. SELECT * FROM cart WHERE user_id = ?
3. SELECT * FROM cart_item WHERE cart_id = ? AND product_id = ?
4. UPDATE cart_item SET quantity = ?
5. UPDATE cart SET total_price = ?
COMMIT // All succeeded!

-- OR --

START TRANSACTION
1. SELECT * FROM product WHERE id = ?
2. SELECT * FROM cart WHERE user_id = ?
3. SELECT * FROM cart_item WHERE cart_id = ? AND product_id = ?
4. UPDATE cart_item SET quantity = ?
5. [ERROR: Constraint violation!]
ROLLBACK // All changes undone!

Layer 4: Data Access Layer (Repositories)

Location: backend/src/main/java/com/automotive/ecommerce/business/repository/

Responsibility: Database access abstraction

Technologies:

  • Spring Data JPA
  • JPA/Hibernate
  • JPQL queries

Key Repositories:

repository/
├── CartRepository.java
├── CartItemRepository.java
├── ProductRepository.java
├── UserRepository.java
└── ... (all extend JpaRepository)

What this layer does:

  1. Provides CRUD operations - Create, Read, Update, Delete
  2. Custom queries - findByUserId, findByEmail, etc.
  3. Abstracts SQL - No SQL in service layer
  4. Query optimization - JOIN FETCH, pagination
  5. Entity management - Persisting and loading entities

What this layer does NOT do:

❌ Business logic ❌ Calculations ❌ Validation ❌ DTOs (returns entities)

Example: CartRepository

// CartRepository.java
@Repository
public interface CartRepository extends JpaRepository<Cart, Long> {

// Inherited from JpaRepository (automatically available):
// - save(Cart cart)
// - findById(Long id)
// - findAll()
// - deleteById(Long id)
// - count()
// - existsById(Long id)
// ... and many more!

// Custom query methods:

// Method name query (Spring Data generates SQL)
Optional<Cart> findByUserId(Long userId);

// Custom JPQL query with JOIN FETCH (optimization)
@Query("SELECT c FROM Cart c LEFT JOIN FETCH c.items ci LEFT JOIN FETCH ci.product WHERE c.user.id = :userId")
Optional<Cart> findByUserIdWithItems(@Param("userId") Long userId);

// Method name query
boolean existsByUserId(Long userId);
}

How Spring Data JPA works:

  1. You write interface:
public interface CartRepository extends JpaRepository<Cart, Long> {
Optional<Cart> findByUserId(Long userId);
}
  1. Spring generates implementation at runtime:
// Pseudo-code (Spring creates this automatically)
public class CartRepositoryImpl implements CartRepository {

@PersistenceContext
private EntityManager entityManager;

public Optional<Cart> findByUserId(Long userId) {
String jpql = "SELECT c FROM Cart c WHERE c.user.id = :userId";
TypedQuery<Cart> query = entityManager.createQuery(jpql, Cart.class);
query.setParameter("userId", userId);
try {
Cart result = query.getSingleResult();
return Optional.of(result);
} catch (NoResultException e) {
return Optional.empty();
}
}
}
  1. Spring injects implementation into service:
@Service
public class CartServiceImpl {
private final CartRepository cartRepository; // Spring-generated implementation!
}

Query Method Naming Convention:

Method NameGenerated SQL
findByUserId(Long id)SELECT * FROM cart WHERE user_id = ?
findByEmail(String email)SELECT * FROM user WHERE email = ?
existsByEmail(String email)SELECT COUNT(*) > 0 FROM user WHERE email = ?
countByUserId(Long id)SELECT COUNT(*) FROM cart WHERE user_id = ?
deleteByUserId(Long id)DELETE FROM cart WHERE user_id = ?

JOIN FETCH Optimization:

// Without JOIN FETCH (N+1 problem):
@Query("SELECT c FROM Cart c WHERE c.user.id = :userId")
Optional<Cart> findByUserId(@Param("userId") Long userId);

// Generates:
// SELECT * FROM cart WHERE user_id = ? -- 1 query
// SELECT * FROM cart_item WHERE cart_id = ? -- 1 query PER cart
// SELECT * FROM product WHERE id = ? -- 1 query PER item
// Total: 1 + N + M queries! SLOW!

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

// Generates:
// SELECT c.*, ci.*, p.* FROM cart c
// LEFT JOIN cart_item ci ON c.id = ci.cart_id
// LEFT JOIN product p ON ci.product_id = p.id
// WHERE c.user.id = ?
// Total: 1 query! FAST!

Repository Pattern Benefits:

  1. Abstraction: Service doesn't know about SQL
  2. Testability: Can mock repositories
  3. Consistency: Uniform API for all entities
  4. Flexibility: Can change database implementation

Layer 5: Database Layer

Location: backend/data/automotive_ecommerce.mv.db (H2 file)

Responsibility: Persistent data storage

Technologies:

  • H2 Database (file-based, not in-memory)
  • SQL

Database Schema:

-- Users table
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
account_locked BOOLEAN DEFAULT FALSE,
email_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP,
updated_at TIMESTAMP
);

-- User roles table (Many-to-Many with enum)
CREATE TABLE user_roles (
user_id BIGINT,
role VARCHAR(50),
PRIMARY KEY (user_id, role),
FOREIGN KEY (user_id) REFERENCES users(id)
);

-- Products table
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
brand VARCHAR(255),
part_number VARCHAR(50) UNIQUE,
category VARCHAR(100),
price DECIMAL(10,2) NOT NULL,
stock_quantity INT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
description TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);

-- Cart table (One-to-One with User)
CREATE TABLE cart (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNIQUE,
total_price DECIMAL(10,2) DEFAULT 0.00,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);

-- Cart items table (Many-to-Many between Cart and Product)
CREATE TABLE cart_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
cart_id BIGINT,
product_id BIGINT,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP,
FOREIGN KEY (cart_id) REFERENCES cart(id),
FOREIGN KEY (product_id) REFERENCES product(id)
);

Entity Relationships:

users (1) ────── (1) cart

│ (1)

│ (Many)
cart_item (Many) ────── (1) product

JPA Entity Mapping:

// User.java
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Maps to: id BIGINT PRIMARY KEY AUTO_INCREMENT

@Column(unique = true, nullable = false)
private String email; // Maps to: email VARCHAR(255) UNIQUE NOT NULL

@OneToOne(mappedBy = "user")
private Cart cart; // Relationship with Cart entity
}

// Cart.java
@Entity
@Table(name = "cart")
public class Cart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne
@JoinColumn(name = "user_id")
private User user; // Foreign key to users.id

@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL)
private List<CartItem> items; // One cart has many items
}

// CartItem.java
@Entity
@Table(name = "cart_item")
public class CartItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne
@JoinColumn(name = "cart_id")
private Cart cart; // Foreign key to cart.id

@ManyToOne
@JoinColumn(name = "product_id")
private Product product; // Foreign key to product.id
}

Database Operations:

Repository MethodGenerated SQL
cartRepository.save(cart)INSERT INTO cart (user_id, total_price, ...) VALUES (?, ?, ...) or UPDATE cart SET ...
cartRepository.findById(id)SELECT * FROM cart WHERE id = ?
cartRepository.findByUserId(userId)SELECT * FROM cart WHERE user_id = ?
cartRepository.delete(cart)DELETE FROM cart WHERE id = ?

Cross-Cutting Concerns

These are concerns that span multiple layers:

Security

Where: All layers

Implementation:

  • JWT authentication filter (intercepts requests)
  • Spring Security configuration
  • @PreAuthorize annotations on controllers
  • User implements UserDetails

Flow:

HTTP Request with JWT token

JWT Authentication Filter

Validate token signature

Extract user details

Create Authentication object

Store in SecurityContext

@PreAuthorize("hasRole('USER')") check

If authorized: Continue to controller

If unauthorized: Return 403 Forbidden

Code Example:

// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // Public
.requestMatchers("/api/cart/**").hasRole("USER") // Protected
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Admin only
.anyRequest().authenticated()
);
return http.build();
}
}

// JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends OncePerRequestFilter {

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

if (token != null && jwtUtil.validateToken(token)) {
// 2. Get user details from token
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

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

// 4. Store in SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}

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

Exception Handling

Where: All layers throw exceptions, handled globally

Implementation:

// GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(CartNotFoundException.class)
public ResponseEntity<String> handleCartNotFound(CartNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}

@ExceptionHandler(InsufficientStockException.class)
public ResponseEntity<String> handleInsufficientStock(InsufficientStockException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An error occurred: " + ex.getMessage());
}
}

Exception Flow:

Service throws InsufficientStockException

Bubbles up to Controller

GlobalExceptionHandler catches it

Returns HTTP 400 with error message

Frontend receives error

Displays toast notification to user

Logging

Where: All layers (especially service)

Implementation:

@Slf4j  // Lombok annotation
@Service
public class CartServiceImpl implements CartService {

@Override
public CartDto addToCart(Long userId, AddToCartRequest request) {
log.info("Adding product {} to cart for user {}", request.getProductId(), userId);

try {
// Business logic...
log.debug("Cart total calculated: {}", cart.getTotalPrice());
return convertToDto(cart);
} catch (Exception e) {
log.error("Error adding to cart for user {}: {}", userId, e.getMessage(), e);
throw e;
}
}
}

Log Levels:

LevelUsageExample
ERRORErrors, exceptionslog.error("Failed to process order", e)
WARNWarnings, potential issueslog.warn("Slow query detected: {}ms", duration)
INFOImportant eventslog.info("User {} logged in", username)
DEBUGDetailed informationlog.debug("Cart total: {}", total)
TRACEVery detailedlog.trace("Query parameters: {}", params)

Transaction Management

Where: Service layer

Implementation:

@Service
@Transactional // Class-level: all methods are transactional
public class CartServiceImpl implements CartService {

@Override
@Transactional(readOnly = true) // Optimization for reads
public CartDto getCart(Long userId) {
// Read-only transaction
}

@Override
@Transactional(
isolation = Isolation.REPEATABLE_READ, // Isolation level
rollbackFor = Exception.class // Rollback on any exception
)
public CartDto addToCart(Long userId, AddToCartRequest request) {
// Multiple database operations in one transaction
// If any fails, ALL are rolled back
}
}

Transaction Isolation Levels:

LevelDescriptionYour Project Uses
READ_UNCOMMITTEDCan see uncommitted changes
READ_COMMITTEDOnly committed data
REPEATABLE_READSame reads in transaction✅ (addToCart)
SERIALIZABLEFull isolation

Request Flow Examples

Example 1: User Login Flow

Complete flow through all layers:

┌─────────────────────────────────────────────────────────────────┐
│ 1. PRESENTATION LAYER (React Frontend) │
└─────────────────────────────────────────────────────────────────┘
User enters email/password and clicks "Login"

Login.js handleSubmit() function

axios.post('/api/auth/login', { email, password })

HTTP POST request sent to backend

┌─────────────────────────────────────────────────────────────────┐
│ 2. SECURITY LAYER (JWT Filter) │
└─────────────────────────────────────────────────────────────────┘
Request arrives at Spring Boot

Passes through JWT filter (no token yet, login is public)

Spring routes to AuthController

┌─────────────────────────────────────────────────────────────────┐
│ 3. API LAYER (Controller) │
└─────────────────────────────────────────────────────────────────┘
AuthController.login() method

@Valid validates LoginRequest (email format, password length)

Calls authService.login(email, password)

┌─────────────────────────────────────────────────────────────────┐
│ 4. BUSINESS LOGIC LAYER (Service) │
└─────────────────────────────────────────────────────────────────┘
AuthService.login()

userRepository.findByEmail(email) ← Repository call

┌─────────────────────────────────────────────────────────────────┐
│ 5. DATA ACCESS LAYER (Repository) │
└─────────────────────────────────────────────────────────────────┘
JPA generates SQL: SELECT * FROM users WHERE email = ?

┌─────────────────────────────────────────────────────────────────┐
│ 6. DATABASE LAYER │
└─────────────────────────────────────────────────────────────────┘
H2 executes query and returns User record

┌─────────────────────────────────────────────────────────────────┐
│ 5. DATA ACCESS LAYER (Repository) │
└─────────────────────────────────────────────────────────────────┘
Repository returns Optional<User>

┌─────────────────────────────────────────────────────────────────┐
│ 4. BUSINESS LOGIC LAYER (Service) │
└─────────────────────────────────────────────────────────────────┘
AuthService receives User

Validate password with BCrypt.matches(rawPassword, user.getPassword())

If valid: Generate JWT token with JwtUtil.generateToken(user.getEmail())

Create LoginResponse DTO with token and user details

Return LoginResponse

┌─────────────────────────────────────────────────────────────────┐
│ 3. API LAYER (Controller) │
└─────────────────────────────────────────────────────────────────┘
AuthController receives LoginResponse

Return ResponseEntity.ok(loginResponse)

Spring converts LoginResponse to JSON

┌─────────────────────────────────────────────────────────────────┐
│ 1. PRESENTATION LAYER (React Frontend) │
└─────────────────────────────────────────────────────────────────┘
axios receives response

Login component extracts token and user data

localStorage.setItem('token', token)

AuthContext.login(token, userData)

navigate('/dashboard')

User sees dashboard

Code Trace:

// 1. Frontend - Login.js
const handleSubmit = async (e) => {
e.preventDefault();
const response = await axios.post('/api/auth/login', { email, password });
// response.data = { token: "eyJ...", email: "user@...", ... }
};
// 2. Controller - AuthController.java
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody @Valid LoginRequest request) {
LoginResponse response = authService.login(request.getEmail(), request.getPassword());
return ResponseEntity.ok(response);
}
// 3. Service - AuthService.java
public LoginResponse login(String email, String password) {
// Call repository
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new BadCredentialsException("Invalid credentials"));

// Validate password
if (!passwordEncoder.matches(password, user.getPassword())) {
user.setFailedLoginAttempts(user.getFailedLoginAttempts() + 1);
userRepository.save(user);
throw new BadCredentialsException("Invalid credentials");
}

// Generate JWT token
String token = jwtUtil.generateToken(user.getEmail());

// Create response
LoginResponse response = new LoginResponse();
response.setToken(token);
response.setEmail(user.getEmail());
response.setFirstName(user.getFirstName());
response.setLastName(user.getLastName());
response.setRoles(user.getRoles());

return response;
}
// 4. Repository - UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// Spring generates: SELECT * FROM users WHERE email = ?

Example 2: Add Product to Cart Flow

┌─────────────────────────────────────────────────────────────────┐
│ 1. PRESENTATION LAYER │
└─────────────────────────────────────────────────────────────────┘
User clicks "Add to Cart" button

onClick handler in UserDashboard.js

axios.post('/api/cart/items', { productId: 1, quantity: 2 }, {
headers: { Authorization: 'Bearer ' + token }
})

┌─────────────────────────────────────────────────────────────────┐
│ 2. SECURITY LAYER │
└─────────────────────────────────────────────────────────────────┘
JwtAuthenticationFilter extracts token

Validates token and loads user

Sets SecurityContext with Authentication

@PreAuthorize("hasRole('USER')") check passes

┌─────────────────────────────────────────────────────────────────┐
│ 3. API LAYER │
└─────────────────────────────────────────────────────────────────┘
CartController.addToCart() method

Extract userId from Authentication object

@Valid validates AddToCartRequest (productId not null, quantity > 0)

Call cartService.addToCart(userId, request)

┌─────────────────────────────────────────────────────────────────┐
│ 4. BUSINESS LOGIC LAYER │
└─────────────────────────────────────────────────────────────────┘
@Transactional starts database transaction

CartServiceImpl.addToCart()

Step 1: productRepository.findByIdAndIsActiveTrue(productId)

┌─────────────────────────────────────────────────────────────────┐
│ 5. DATA ACCESS LAYER │
└─────────────────────────────────────────────────────────────────┘
SQL: SELECT * FROM product WHERE id = ? AND is_active = true

┌─────────────────────────────────────────────────────────────────┐
│ 6. DATABASE │
└─────────────────────────────────────────────────────────────────┘
Returns Product record

┌─────────────────────────────────────────────────────────────────┐
│ 4. BUSINESS LOGIC LAYER (continued) │
└─────────────────────────────────────────────────────────────────┘
Step 2: Validate stock with validateStock(product, quantity)

if (product.getStockQuantity() < quantity) throw InsufficientStockException

Step 3: cartRepository.findByUserIdWithItems(userId)

SQL: SELECT c.*, ci.*, p.* FROM cart c
LEFT JOIN cart_item ci ON c.id = ci.cart_id
LEFT JOIN product p ON ci.product_id = p.id
WHERE c.user.id = ?

Step 4: Check if product already in cart

cartItemRepository.findByCartIdAndProductId(cartId, productId)

SQL: SELECT * FROM cart_item WHERE cart_id = ? AND product_id = ?

If exists: Update quantity
If not: Create new CartItem

Step 5: calculateCartTotal(cart)

Business calculation: sum all (item.price * item.quantity)

Step 6: cartRepository.save(cart)

SQL: UPDATE cart SET total_price = ?, updated_at = ? WHERE id = ?
SQL: INSERT INTO cart_item (...) VALUES (...) OR UPDATE cart_item ...

@Transactional commits transaction (all changes persisted)

Step 7: convertToDto(cart)

Return CartDto

┌─────────────────────────────────────────────────────────────────┐
│ 3. API LAYER │
└─────────────────────────────────────────────────────────────────┘
CartController receives CartDto

Return ResponseEntity.ok(cartDto)

Spring converts CartDto to JSON

┌─────────────────────────────────────────────────────────────────┐
│ 1. PRESENTATION LAYER │
└─────────────────────────────────────────────────────────────────┘
axios receives response

Update cart count in Header (re-render)

Show success toast notification

User sees updated cart

Architecture Benefits and Trade-offs

Benefits

1. Separation of Concerns

  • Each layer has a single, clear responsibility
  • Changes in one layer don't affect others
  • Easier to understand and maintain

2. Testability

  • Each layer can be tested independently
  • Mock dependencies easily
  • Unit tests, integration tests, E2E tests

3. Flexibility

  • Can swap implementations (e.g., different databases)
  • Can add caching layer
  • Can change UI without touching backend

4. Team Collaboration

  • Frontend and backend teams work independently
  • Clear interfaces between layers
  • Parallel development possible

5. Reusability

  • Business logic can be used by multiple UIs
  • Service methods reusable across controllers
  • Repository methods reusable across services

6. Maintainability

  • Clear structure makes code easy to navigate
  • Bugs easier to locate and fix
  • Changes localized to specific layers

Trade-offs

1. Complexity

  • More files and classes
  • More abstractions to understand
  • Steeper learning curve for beginners

2. Performance Overhead

  • Multiple layers add slight overhead
  • DTO conversion takes time
  • More method calls between layers

3. Initial Development Time

  • More upfront design work
  • More boilerplate code (interfaces, DTOs)
  • Slower to implement simple features

4. Over-engineering Risk

  • Can be overkill for small projects
  • Too many abstractions for simple CRUD
  • Balance needed

When to Use N-Tier Architecture

✅ Good For:

  • Medium to large applications
  • Team projects with multiple developers
  • Long-term projects requiring maintenance
  • Projects with complex business logic
  • Projects needing different frontends (web, mobile, API)

❌ Not Ideal For:

  • Proof of concepts
  • Throwaway prototypes
  • Very simple CRUD applications
  • Solo projects with simple requirements

Summary

Your e-commerce project follows a clean 5-tier architecture:

1. React Frontend (Presentation)

2. Spring Controllers (API)

3. Spring Services (Business Logic)

4. Spring Repositories (Data Access)

5. H2 Database (Persistence)

Key Principles:

  1. One-way dependencies: Each layer only depends on the layer below
  2. Clear responsibilities: Each layer has a specific job
  3. Abstraction: Layers depend on interfaces, not implementations
  4. Separation: Changes in one layer don't affect others

Request Flow Pattern:

User Action
→ Frontend (React)
→ HTTP Request (Axios)
→ Security Filter (JWT)
→ Controller (HTTP → Java)
→ Service (Business Logic)
→ Repository (Data Access)
→ Database (SQL)
← Reverse flow with response

Why This Architecture?

  • ✅ Maintainable
  • ✅ Testable
  • ✅ Scalable
  • ✅ Flexible
  • ✅ Professional

Your project demonstrates industry-standard architecture patterns used in production applications worldwide. Understanding these patterns makes you a better developer and prepares you for real-world software development.


Next Steps

  1. Trace a Request: Use debugger to follow a request through all layers
  2. Draw Diagrams: Sketch the architecture for different features
  3. Identify Patterns: Find where SOLID principles are applied in each layer
  4. Add Features: Implement new features following the architecture
  5. Experiment: Try breaking the architecture rules and see what happens
  6. Read Documentation: Spring Boot, Spring Data JPA, React official docs

Remember: Architecture is about trade-offs. There's no perfect architecture, only appropriate ones for specific contexts. Your project's architecture is well-suited for an e-commerce application of this scale.

Happy learning! 🚀