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
- Introduction to N-Tier Architecture
- Your Project's Architecture
- Layer 1: Presentation Layer (Frontend)
- Layer 2: API Layer (Controllers)
- Layer 3: Business Logic Layer (Services)
- Layer 4: Data Access Layer (Repositories)
- Layer 5: Database Layer
- Cross-Cutting Concerns
- Request Flow Examples
- 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:
- Renders UI - Displays data to users
- Handles user input - Forms, buttons, interactions
- Makes API calls - Sends requests to backend
- Manages local state - UI state (loading, errors)
- 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:
- Receives HTTP requests - GET, POST, PUT, DELETE
- Validates input - @Valid annotations
- Extracts authentication - From JWT tokens
- Calls service layer - Delegates business logic
- Formats responses - Convert to JSON
- 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:
| Concern | Controller'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:
- Implements business rules - Stock validation, pricing logic
- Coordinates between repositories - Multiple database operations
- Manages transactions - Ensures data consistency
- Performs calculations - Cart totals, discounts
- Converts entities to DTOs - Data transformation
- 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:
| Concern | Service'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:
- Provides CRUD operations - Create, Read, Update, Delete
- Custom queries - findByUserId, findByEmail, etc.
- Abstracts SQL - No SQL in service layer
- Query optimization - JOIN FETCH, pagination
- 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:
- You write interface:
public interface CartRepository extends JpaRepository<Cart, Long> {
Optional<Cart> findByUserId(Long userId);
}
- 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();
}
}
}
- Spring injects implementation into service:
@Service
public class CartServiceImpl {
private final CartRepository cartRepository; // Spring-generated implementation!
}
Query Method Naming Convention:
| Method Name | Generated 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:
- Abstraction: Service doesn't know about SQL
- Testability: Can mock repositories
- Consistency: Uniform API for all entities
- 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 Method | Generated 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:
| Level | Usage | Example |
|---|---|---|
| ERROR | Errors, exceptions | log.error("Failed to process order", e) |
| WARN | Warnings, potential issues | log.warn("Slow query detected: {}ms", duration) |
| INFO | Important events | log.info("User {} logged in", username) |
| DEBUG | Detailed information | log.debug("Cart total: {}", total) |
| TRACE | Very detailed | log.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:
| Level | Description | Your Project Uses |
|---|---|---|
| READ_UNCOMMITTED | Can see uncommitted changes | ❌ |
| READ_COMMITTED | Only committed data | ❌ |
| REPEATABLE_READ | Same reads in transaction | ✅ (addToCart) |
| SERIALIZABLE | Full 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:
- One-way dependencies: Each layer only depends on the layer below
- Clear responsibilities: Each layer has a specific job
- Abstraction: Layers depend on interfaces, not implementations
- 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
- Trace a Request: Use debugger to follow a request through all layers
- Draw Diagrams: Sketch the architecture for different features
- Identify Patterns: Find where SOLID principles are applied in each layer
- Add Features: Implement new features following the architecture
- Experiment: Try breaking the architecture rules and see what happens
- 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! 🚀