Java Notes
Build a complete E-Commerce backend REST API with Spring Boot - product catalog, cart management, order processing, payment integration, and admin panel.
Project Overview
The E-Commerce Backend is a production-ready REST API built with Spring Boot that powers a complete online shopping platform. It handles product catalog management, user authentication, shopping cart operations, order processing, payment integration (Razorpay/Stripe), inventory tracking, coupon/discount management, and an admin dashboard API.
This project is ideal for:
- Java developers building real-world backend systems
- Anyone preparing for backend developer interviews at e-commerce companies
- Students wanting a comprehensive portfolio project
- Developers learning microservices-ready monolithic architecture
The system follows clean architecture principles, implements role-based access control (Admin, Seller, Customer), supports multi-vendor product listings, and uses Redis caching for high-traffic product pages.
Features
- User Registration & Authentication — JWT-based auth with email verification and OAuth2 (Google)
- Product Catalog — CRUD operations with categories, sub-categories, and product variants (size, color)
- Advanced Product Search — Elasticsearch-powered full-text search with filters and facets
- Shopping Cart — Add/remove items, update quantities, merge guest cart on login
- Wishlist — Save products for later with stock alerts
- Order Management — Place orders, track status (Placed → Shipped → Delivered)
- Payment Integration — Razorpay/Stripe with webhook verification and refund support
- Inventory Management — Stock tracking with low-stock alerts and reservation during checkout
- Coupon & Discounts — Percentage/flat discounts, minimum order value, usage limits
- Product Reviews & Ratings — Star ratings with verified purchase badge
- Address Management — Multiple shipping addresses per user with default selection
- Order Invoice — Auto-generated PDF invoices with GST calculation
- Admin Dashboard API — Sales analytics, top products, revenue reports
- Seller Portal — Multi-vendor support with seller product management
- Image Upload — Multiple product images with thumbnail generation
- Category Management — Hierarchical categories with breadcrumb support
- Pagination & Sorting — Efficient paginated responses for all list endpoints
- Rate Limiting — API rate limiting to prevent abuse
- Email Notifications — Order confirmation, shipping updates, password reset
- Audit Logging — Track all admin actions with timestamps and user info
Project Structure
Database Schema
products Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique product ID |
| name | VARCHAR(200) | NOT NULL | Product name |
| slug | VARCHAR(220) | UNIQUE, NOT NULL | URL-friendly name |
| description | TEXT | Detailed description | |
| short_description | VARCHAR(500) | Brief description | |
| price | DECIMAL(10,2) | NOT NULL | Original price (MRP) |
| selling_price | DECIMAL(10,2) | NOT NULL | Selling price |
| stock_quantity | INT | DEFAULT 0 | Available stock |
| category_id | BIGINT | FOREIGN KEY | Product category |
| seller_id | BIGINT | FOREIGN KEY | Seller reference |
| brand | VARCHAR(100) | Brand name | |
| sku | VARCHAR(50) | UNIQUE | Stock keeping unit |
| weight | DECIMAL(5,2) | Weight in kg | |
| is_active | BOOLEAN | DEFAULT TRUE | Published status |
| average_rating | DECIMAL(2,1) | DEFAULT 0.0 | Average review rating |
| total_reviews | INT | DEFAULT 0 | Total review count |
| created_at | TIMESTAMP | NOT NULL | Creation timestamp |
| updated_at | TIMESTAMP | NOT NULL | Last update timestamp |
orders Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique order ID |
| order_number | VARCHAR(20) | UNIQUE, NOT NULL | Human-readable order number |
| user_id | BIGINT | FOREIGN KEY | Customer reference |
| total_amount | DECIMAL(12,2) | NOT NULL | Order total before discount |
| discount_amount | DECIMAL(10,2) | DEFAULT 0 | Discount applied |
| final_amount | DECIMAL(12,2) | NOT NULL | Amount charged |
| coupon_id | BIGINT | FOREIGN KEY (nullable) | Applied coupon |
| shipping_address_id | BIGINT | FOREIGN KEY | Delivery address |
| status | ENUM | NOT NULL | PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED |
| payment_status | ENUM | NOT NULL | PENDING, COMPLETED, FAILED, REFUNDED |
| tracking_number | VARCHAR(50) | Shipping tracking ID | |
| ordered_at | TIMESTAMP | NOT NULL | Order placement time |
| delivered_at | TIMESTAMP | Delivery time |
cart_items Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique ID |
| cart_id | BIGINT | FOREIGN KEY | Cart reference |
| product_id | BIGINT | FOREIGN KEY | Product reference |
| quantity | INT | NOT NULL, CHECK > 0 | Item quantity |
| unit_price | DECIMAL(10,2) | NOT NULL | Price at time of adding |
| added_at | TIMESTAMP | NOT NULL | When item was added |
Entity Relationships
Key Code Snippets
Product Entity
package com.wohotech.ecommerce.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "products", indexes = {
@Index(name = "idx_product_category", columnList = "category_id"),
@Index(name = "idx_product_seller", columnList = "seller_id"),
@Index(name = "idx_product_slug", columnList = "slug")
})
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(unique = true, nullable = false, length = 220)
private String slug;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "short_description", length = 500)
private String shortDescription;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price; // MRP
@Column(name = "selling_price", nullable = false, precision = 10, scale = 2)
private BigDecimal sellingPrice;
@Column(name = "stock_quantity")
@Builder.Default
private Integer stockQuantity = 0;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seller_id")
private User seller;
@Column(length = 100)
private String brand;
@Column(unique = true, length = 50)
private String sku;
@Column(precision = 5, scale = 2)
private BigDecimal weight;
@Column(name = "is_active")
@Builder.Default
private Boolean isActive = true;
@Column(name = "average_rating", precision = 2, scale = 1)
@Builder.Default
private BigDecimal averageRating = BigDecimal.ZERO;
@Column(name = "total_reviews")
@Builder.Default
private Integer totalReviews = 0;
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<ProductImage> images = new ArrayList<>();
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
@Builder.Default
private List<Review> reviews = new ArrayList<>();
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Business methods
public int getDiscountPercentage() {
if (price.compareTo(BigDecimal.ZERO) == 0) return 0;
return price.subtract(sellingPrice)
.multiply(BigDecimal.valueOf(100))
.divide(price, 0, java.math.RoundingMode.HALF_UP)
.intValue();
}
public boolean isInStock() {
return stockQuantity > 0;
}
public void decreaseStock(int quantity) {
if (this.stockQuantity < quantity) {
throw new RuntimeException("Insufficient stock for product: " + name);
}
this.stockQuantity -= quantity;
}
}Order Service Implementation
Product Controller
package com.wohotech.ecommerce.controller;
import com.wohotech.ecommerce.dto.request.ProductCreateRequest;
import com.wohotech.ecommerce.dto.response.PagedResponse;
import com.wohotech.ecommerce.dto.response.ProductResponse;
import com.wohotech.ecommerce.service.ProductService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.util.List;
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
@Tag(name = "Products", description = "Product catalog management APIs")
public class ProductController {
private final ProductService productService;
@GetMapping
@Operation(summary = "Get all products with pagination and filters")
public ResponseEntity<PagedResponse<ProductResponse>> getAllProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "12") int size,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) String brand,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "desc") String sortDir) {
return ResponseEntity.ok(productService.getAllProducts(
page, size, categoryId, brand, minPrice, maxPrice, sortBy, sortDir));
}
@GetMapping("/{slug}")
@Operation(summary = "Get product details by slug")
public ResponseEntity<ProductResponse> getProductBySlug(@PathVariable String slug) {
return ResponseEntity.ok(productService.getProductBySlug(slug));
}
@GetMapping("/search")
@Operation(summary = "Search products using Elasticsearch")
public ResponseEntity<PagedResponse<ProductResponse>> searchProducts(
@RequestParam String q,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "12") int size) {
return ResponseEntity.ok(productService.searchProducts(q, page, size));
}
@PostMapping
@PreAuthorize("hasRole('ADMIN') or hasRole('SELLER')")
@Operation(summary = "Create a new product (Admin/Seller)")
public ResponseEntity<ProductResponse> createProduct(
@Valid @RequestBody ProductCreateRequest request) {
return new ResponseEntity<>(productService.createProduct(request), HttpStatus.CREATED);
}
@PostMapping("/{productId}/images")
@PreAuthorize("hasRole('ADMIN') or hasRole('SELLER')")
@Operation(summary = "Upload product images")
public ResponseEntity<List<String>> uploadImages(
@PathVariable Long productId,
@RequestParam("files") List<MultipartFile> files) {
return ResponseEntity.ok(productService.uploadImages(productId, files));
}
@PutMapping("/{productId}")
@PreAuthorize("hasRole('ADMIN') or hasRole('SELLER')")
@Operation(summary = "Update product details")
public ResponseEntity<ProductResponse> updateProduct(
@PathVariable Long productId,
@Valid @RequestBody ProductCreateRequest request) {
return ResponseEntity.ok(productService.updateProduct(productId, request));
}
@DeleteMapping("/{productId}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Delete a product (Admin only)")
public ResponseEntity<Void> deleteProduct(@PathVariable Long productId) {
productService.deleteProduct(productId);
return ResponseEntity.noContent().build();
}
}Product Repository with Custom Queries
package com.wohotech.ecommerce.repository;
import com.wohotech.ecommerce.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> {
Optional<Product> findBySlug(String slug);
Optional<Product> findBySku(String sku);
Page<Product> findByCategoryIdAndIsActiveTrue(Long categoryId, Pageable pageable);
Page<Product> findBySellingPriceBetweenAndIsActiveTrue(
BigDecimal minPrice, BigDecimal maxPrice, Pageable pageable);
@Query("SELECT p FROM Product p WHERE p.isActive = true AND p.stockQuantity > 0 " +
"AND p.category.id = :categoryId ORDER BY p.averageRating DESC")
List<Product> findTopRatedByCategory(@Param("categoryId") Long categoryId, Pageable pageable);
@Query("SELECT p FROM Product p WHERE p.stockQuantity < :threshold AND p.isActive = true")
List<Product> findLowStockProducts(@Param("threshold") int threshold);
@Query("SELECT p.brand, COUNT(p) FROM Product p WHERE p.category.id = :categoryId " +
"GROUP BY p.brand")
List<Object[]> findBrandCountByCategory(@Param("categoryId") Long categoryId);
@Query(value = "SELECT * FROM products WHERE is_active = true " +
"ORDER BY (total_reviews * average_rating) DESC LIMIT :limit",
nativeQuery = true)
List<Product> findTrendingProducts(@Param("limit") int limit);
}API Endpoints
| Method | URL | Description | Auth |
|---|---|---|---|
| POST | /api/v1/auth/register | Register customer | No |
| POST | /api/v1/auth/login | Login (returns JWT) | No |
| POST | /api/v1/auth/refresh | Refresh JWT token | Yes |
| GET | /api/v1/products | List products (paginated, filterable) | No |
| GET | /api/v1/products/{slug} | Product details | No |
| GET | /api/v1/products/search?q= | Full-text search | No |
| POST | /api/v1/products | Create product | Admin/Seller |
| PUT | /api/v1/products/{id} | Update product | Admin/Seller |
| DELETE | /api/v1/products/{id} | Delete product | Admin |
| GET | /api/v1/categories | List categories (tree) | No |
| GET | /api/v1/cart | View cart | Customer |
| POST | /api/v1/cart/items | Add item to cart | Customer |
| PUT | /api/v1/cart/items/{itemId} | Update cart item quantity | Customer |
| DELETE | /api/v1/cart/items/{itemId} | Remove item from cart | Customer |
| POST | /api/v1/orders | Place order | Customer |
| GET | /api/v1/orders | List user's orders | Customer |
| GET | /api/v1/orders/{orderNumber} | Order details | Customer |
| PUT | /api/v1/orders/{id}/cancel | Cancel order | Customer |
| POST | /api/v1/payments/verify | Verify payment webhook | System |
| POST | /api/v1/coupons/validate | Validate coupon code | Customer |
| POST | /api/v1/reviews | Submit review | Customer |
| GET | /api/v1/products/{id}/reviews | Get product reviews | No |
| GET | /api/v1/admin/dashboard | Sales dashboard data | Admin |
| GET | /api/v1/admin/orders | All orders management | Admin |
Sample Request & Response
POST /api/v1/orders
Request:
{
"shippingAddressId": 5,
"couponCode": "FIRST20",
"paymentMethod": "RAZORPAY",
"notes": "Please deliver before 5 PM"
}Response (201 Created):
How to Run
Prerequisites
- Java 17+, Maven 3.9+, MySQL 8.0, Redis 7.x
- Elasticsearch 8.x (for product search)
- Razorpay account (for payments)
Setup Steps
Step 1: Clone and configure
git clone https://github.com/wohotech/ecommerce-backend.git
cd ecommerce-backendStep 2: Create database
CREATE DATABASE ecommerce_db;
CREATE USER 'ecom_user'@'localhost' IDENTIFIED BY 'ecom@123';
GRANT ALL PRIVILEGES ON ecommerce_db.* TO 'ecom_user'@'localhost';Step 3: Configure application.yml
Step 4: Run with Docker Compose
docker-compose up -d # Start MySQL, Redis, Elasticsearch
mvn clean install
mvn spring-boot:runStep 5: Access
- API:
http://localhost:8080/api/v1 - Swagger:
http://localhost:8080/swagger-ui.html - Admin: Use
/auth/loginwith admin credentials from data.sql
Screenshots Description
- Swagger UI — All 25+ endpoints documented with request/response schemas
- Product Listing — Paginated products with filters (category, price range, brand)
- Product Details — Full product page API response with images, reviews, and related products
- Shopping Cart — Cart with multiple items showing subtotal, discount, and final amount
- Order Placement — Order confirmation with payment link and order number
- Payment Flow — Razorpay payment page integration and webhook verification
- Admin Dashboard — Revenue charts, top products, and order status distribution
- Elasticsearch — Search results with relevance scoring and filter facets
Future Enhancements
- Recommendation Engine — Collaborative filtering for "Customers also bought"
- Flash Sales — Time-limited deals with inventory countdown
- Multi-language — i18n support for product descriptions
- Delivery Tracking — Real-time GPS-based delivery tracking
- Return & Exchange — Return request workflow with refund processing
- Subscription Orders — Subscribe-and-save for recurring deliveries
- A/B Testing — Test different pricing strategies
- GraphQL API — Alternative GraphQL endpoint for mobile clients
- Event Sourcing — Kafka-based event streams for analytics pipeline
- AI Chatbot — Product recommendations via conversational AI
Interview Questions
Q1: How do you handle race conditions during checkout (two users buying last item)?
Answer: We use pessimistic locking (SELECT ... FOR UPDATE) when decreasing stock during order placement. The @Transactional annotation ensures atomicity. If two users try to buy the last item simultaneously, one gets the lock and succeeds; the other's transaction waits, then finds stock = 0 and throws OutOfStockException. We also use @Version for optimistic locking as a secondary safeguard.
Q2: How does the coupon validation work?
Answer: Coupons have constraints: expiry date, minimum order value, maximum discount cap, usage limit per user, and total usage limit. During validation: (1) Check if coupon exists and is active, (2) Verify not expired, (3) Check minimum order value, (4) Count user's past usage, (5) Check total usage limit. The discount is calculated as min(percentage * orderAmount, maxDiscountCap). We use Redis to track real-time usage counts for high-traffic scenarios.
Q3: Why use Elasticsearch for product search instead of MySQL LIKE queries?
Answer: MySQL LIKE '%term%' doesn't use indexes (full table scan), doesn't support relevance scoring, and can't handle typos or synonyms. Elasticsearch provides: (1) Inverted index for fast full-text search, (2) BM25 relevance scoring, (3) Fuzzy matching for typos, (4) Faceted search (filter by brand, price range, rating), (5) Autocomplete with edge n-grams. We sync products to ES via event listeners on save/update.
Q4: How do you handle payment failures and ensure data consistency?
Answer: We follow the Saga pattern: (1) Order is created with PAYMENT_PENDING status, (2) Payment link is sent to user, (3) Razorpay sends webhook on success/failure, (4) We verify the webhook signature for authenticity, (5) On success: update order to CONFIRMED, (6) On failure: release reserved stock, mark order as FAILED. We use idempotency keys to handle duplicate webhooks. A scheduled job cleans up orders pending payment for more than 30 minutes.
Q5: How is Redis used in this project?
Answer: Redis serves multiple purposes: (1) Product caching — Frequently viewed products cached with 1-hour TTL to reduce DB load, (2) Session storage — JWT blacklist for logged-out tokens, (3) Rate limiting — Sliding window counter per user per endpoint, (4) Cart for guests — Anonymous user carts stored in Redis with 7-day TTL, (5) Coupon usage counter — Atomic increment for real-time usage tracking, (6) Distributed locks — Redisson for critical sections in multi-instance deployment.
Q6: Explain the pagination strategy used in this project.
Answer: We use Spring Data's Pageable with offset-based pagination for general listing and cursor-based pagination for infinite scroll. The response includes: content (items), page (current), size (per page), totalElements, totalPages, hasNext, hasPrevious. For Elasticsearch, we use search_after for deep pagination (beyond 10,000 results). We also implement database-level pagination with LIMIT/OFFSET to avoid fetching all rows.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for E-Commerce Backend.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, projects, ecommerce, backend
Related Java Master Course Topics