Mini Project 1 - E-Commerce Backend
In this tutorial, you will learn about Mini Project 1. We cover key concepts, practical examples, and best practices to help you master this topic.
Project Overview
This mini project brings together everything you have learned about Spring Boot, JPA, REST APIs, and enterprise patterns. You will build a production-quality e-commerce backend that handles products, shopping carts, orders, user management, and payment processing. This project demonstrates how to structure a real-world application with proper separation of concerns, validation, error handling, security, and testing.
The project is designed to be built incrementally. Each module adds functionality on top of the previous one, and you can test each layer as you go.
flowchart TB
subgraph API[REST API Layer]
PC[ProductController]
CC[CartController]
OC[OrderController]
UC[UserController]
AC[AuthController]
end
subgraph Service[Service Layer]
PS[ProductService]
CS[CartService]
OS[OrderService]
US[UserService]
AS[AuthService]
end
subgraph Repository[Data Layer]
PR[ProductRepository]
CR[CartRepository]
OR[OrderRepository]
UR[UserRepository]
end
subgraph Infrastructure
Security[Spring Security + JWT]
Cache[Redis Cache]
Events[Event Publishing]
Validation[Jakarta Validation]
Docs[OpenAPI Docs]
end
API --> Service
Service --> Repository
Service --> Infrastructure
Repository --> DB[(PostgreSQL)]
Cache --> Redis[(Redis)]
Project Setup
Start a Spring Boot Project
Create a new Spring Boot 3.3+ project with the following dependencies:
- Spring Web
- Spring Data JPA
- Spring Security
- Spring Validation
- PostgreSQL Driver
- Flyway Migration
- Redis (for caching)
- Lombok
- Springdoc OpenAPI
Application Properties
spring:
datasource:
url: jdbc:postgresql://localhost:5432/ecommerce
username: ${DB_USERNAME:ec_app}
password: ${DB_PASSWORD:ec_secret}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
flyway:
locations: classpath:db/migration
cache:
type: redis
redis:
time-to-live: 3600000
jackson:
serialization:
write-dates-as-timestamps: false
default-property-inclusion: non_null
app:
jwt:
secret: ${JWT_SECRET:base64-encoded-secret-at-least-256-bits-long}
expiration-ms: 86400000
Database Schema (Flyway Migration)
-- V1__initial_schema.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'CUSTOMER',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE categories (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(150) UNIQUE NOT NULL,
parent_id BIGINT REFERENCES categories(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
slug VARCHAR(250) UNIQUE NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INTEGER NOT NULL DEFAULT 0,
category_id BIGINT REFERENCES categories(id),
image_url VARCHAR(500),
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT positive_price CHECK (price >= 0),
CONSTRAINT non_negative_stock CHECK (stock_quantity >= 0)
);
CREATE TABLE carts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE cart_items (
id BIGSERIAL PRIMARY KEY,
cart_id BIGINT REFERENCES carts(id) ON DELETE CASCADE,
product_id BIGINT REFERENCES products(id),
quantity INTEGER NOT NULL,
CONSTRAINT positive_quantity CHECK (quantity > 0)
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
total_amount DECIMAL(12, 2) NOT NULL,
shipping_address TEXT NOT NULL,
payment_method VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT REFERENCES products(id),
product_name VARCHAR(200) NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
quantity INTEGER NOT NULL,
subtotal DECIMAL(12, 2) NOT NULL
);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
Entity Model
@Entity
@Table(name = "products")
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String slug;
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "stock_quantity", nullable = false)
private int stockQuantity;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@Column(name = "image_url")
private String imageUrl;
private boolean active = true;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
public void reduceStock(int quantity) {
if (stockQuantity < quantity) {
throw new InsufficientStockException(id, stockQuantity, quantity);
}
stockQuantity -= quantity;
}
}
Service Layer with Business Logic
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final CartRepository cartRepository;
private final ProductRepository productRepository;
private final PaymentGateway paymentGateway;
private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepository,
CartRepository cartRepository,
ProductRepository productRepository,
PaymentGateway paymentGateway,
ApplicationEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.cartRepository = cartRepository;
this.productRepository = productRepository;
this.paymentGateway = paymentGateway;
this.eventPublisher = eventPublisher;
}
public OrderResponse checkout(Long userId, CheckoutRequest request) {
Cart cart = cartRepository.findByUserIdWithItems(userId)
.orElseThrow(() -> new CartNotFoundException(userId));
if (cart.getItems().isEmpty()) {
throw new EmptyCartException(userId);
}
// Validate stock availability
for (CartItem item : cart.getItems()) {
Product product = productRepository.findById(item.getProduct().getId())
.orElseThrow(() -> new ProductNotFoundException(item.getProduct().getId()));
if (product.getStockQuantity() < item.getQuantity()) {
throw new InsufficientStockException(product.getId(),
product.getStockQuantity(), item.getQuantity());
}
}
// Create order
Order order = new Order();
order.setUserId(userId);
order.setStatus(OrderStatus.PENDING);
order.setShippingAddress(request.shippingAddress());
order.setPaymentMethod(request.paymentMethod());
BigDecimal total = BigDecimal.ZERO;
List<OrderItem> orderItems = new ArrayList<>();
for (CartItem item : cart.getItems()) {
Product product = item.getProduct();
OrderItem orderItem = new OrderItem();
orderItem.setOrder(order);
orderItem.setProductId(product.getId());
orderItem.setProductName(product.getName());
orderItem.setUnitPrice(product.getPrice());
orderItem.setQuantity(item.getQuantity());
orderItem.setSubtotal(product.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity())));
total = total.add(orderItem.getSubtotal());
orderItems.add(orderItem);
// Reduce stock
product.reduceStock(item.getQuantity());
}
order.setItems(orderItems);
order.setTotalAmount(total);
// Process payment
PaymentResult payment = paymentGateway.charge(
userId, total, request.paymentMethod());
if (payment.isSuccess()) {
order.setStatus(OrderStatus.CONFIRMED);
} else {
order.setStatus(OrderStatus.PAYMENT_FAILED);
}
Order savedOrder = orderRepository.save(order);
// Clear cart
cartRepository.delete(cart);
// Publish events
eventPublisher.publishEvent(new OrderCreatedEvent(savedOrder.getId(), userId));
return OrderResponse.from(savedOrder);
}
}
Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm ->
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/products/**",
"/actuator/health", "/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Testing Strategy
@SpringBootTest
@AutoConfigureMockMvc
class OrderServiceIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private OrderRepository orderRepository;
@MockitoBean
private PaymentGateway paymentGateway;
@Test
@DisplayName("Should create order from cart items")
void checkoutSuccess() throws Exception {
// Given
when(paymentGateway.charge(anyLong(), any(), anyString()))
.thenReturn(new PaymentResult(true, "txn_123"));
String token = obtainAccessToken("customer@test.com", "password");
// When/Then
mockMvc.perform(post("/api/orders/checkout")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"shippingAddress": "123 Main St",
"paymentMethod": "CREDIT_CARD"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.status").value("CONFIRMED"))
.andExpect(jsonPath("$.totalAmount").isNumber());
assertThat(orderRepository.count()).isEqualTo(1);
}
}
What You Have Built
- Multi-module Spring Boot application with Clean Architecture
- JPA entities with relationships and Flyway migrations
- REST controllers with validation and error handling
- JWT-based authentication and role-based authorization
- Service layer with business logic, stock management, and payment integration
- Event-Driven Architecture for async notifications
- Redis caching for product catalog
- OpenAPI documentation
- Integration and unit tests
Extension Ideas
- Add Websocket-based order status notifications
- Implement a recommendation engine based on purchase history
- Add Stripe or PayPal payment gateway integration
- Build an admin dashboard with metrics
- Add Elasticsearch for full-text product search
- Implement a loyalty points system
FAQ
What's Next
You have built a comprehensive e-commerce backend. In the next mini project, you will build a real-time chat application using WebSockets, exploring a different communication paradigm.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro