Building a Scalable E-Commerce Platform Backend in Java

Article

Building a robust e-commerce platform requires careful architecture design, scalable components, and secure payment processing. This comprehensive guide covers building a complete e-commerce backend in Java using Spring Boot, microservices architecture, and modern cloud technologies.


System Architecture Overview

Microservices Architecture:

  • API Gateway: Single entry point with routing and security
  • User Service: Authentication, profiles, and preferences
  • Product Service: Catalog management, search, and inventory
  • Order Service: Order processing and fulfillment
  • Payment Service: Payment processing and transactions
  • Notification Service: Email, SMS, and push notifications
  • Shipping Service: Shipping calculations and tracking

Technology Stack:

  • Spring Boot 3.x - Core framework
  • Spring Cloud - Microservices ecosystem
  • PostgreSQL - Primary database
  • Redis - Caching and sessions
  • Apache Kafka - Event streaming
  • Elasticsearch - Product search
  • Docker & Kubernetes - Containerization and orchestration

Project Structure and Dependencies

1. Parent POM (Maven)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ecommerce</groupId>
<artifactId>ecommerce-platform</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>api-gateway</module>
<module>user-service</module>
<module>product-service</module>
<module>order-service</module>
<module>payment-service</module>
<module>notification-service</module>
<module>shipping-service</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.1.0</spring-boot.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

2. Common Dependencies Module

<!-- common/pom.xml -->
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.0</version>
</dependency>
<!-- Messaging -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- Monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>

Domain Models and Common Components

1. Common DTOs and Entities

// common/src/main/java/com/ecommerce/common/dto/
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private String timestamp;
public ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
this.timestamp = Instant.now().toString();
}
// Static factory methods
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "Success", data);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
// Getters and setters
// ... 
}
public class PaginatedResponse<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean last;
// Constructors, getters, setters
}
// Common entity base class
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreationTimestamp
@Column(updatable = false)
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
@Version
private Long version;
// Getters and setters
}

2. Common Enums

public enum OrderStatus {
PENDING,
CONFIRMED,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED,
REFUNDED
}
public enum PaymentStatus {
PENDING,
PROCESSING,
COMPLETED,
FAILED,
REFUNDED,
CANCELLED
}
public enum PaymentMethod {
CREDIT_CARD,
DEBIT_CARD,
PAYPAL,
STRIPE,
APPLE_PAY,
GOOGLE_PAY
}
public enum UserRole {
CUSTOMER,
SELLER,
ADMIN,
MODERATOR
}

User Service Implementation

1. User Entity and DTOs

@Entity
@Table(name = "users")
public class User extends BaseEntity {
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
private String firstName;
private String lastName;
@Enumerated(EnumType.STRING)
private UserRole role;
private boolean enabled;
private boolean emailVerified;
@Embedded
private Address address;
@ElementCollection
@CollectionTable(name = "user_preferences", joinColumns = @JoinColumn(name = "user_id"))
private Map<String, String> preferences = new HashMap<>();
// Constructors, getters, setters
}
@Embeddable
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
private String country;
// Constructors, getters, setters
}
// DTOs
public class UserRegistrationRequest {
@Email
@NotBlank
private String email;
@NotBlank
@Size(min = 8)
private String password;
@NotBlank
private String firstName;
@NotBlank
private String lastName;
// Getters and setters
}
public class UserResponse {
private Long id;
private String email;
private String firstName;
private String lastName;
private UserRole role;
private Address address;
private Map<String, String> preferences;
// Constructors, getters, setters
}

2. User Service Implementation

@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
private final KafkaTemplate<String, Object> kafkaTemplate;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder,
JwtTokenProvider tokenProvider, KafkaTemplate<String, Object> kafkaTemplate) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.tokenProvider = tokenProvider;
this.kafkaTemplate = kafkaTemplate;
}
public UserResponse registerUser(UserRegistrationRequest request) {
// Check if email already exists
if (userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("Email already registered");
}
// Create new user
User user = new User();
user.setEmail(request.getEmail());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setRole(UserRole.CUSTOMER);
user.setEnabled(true);
user.setEmailVerified(false);
User savedUser = userRepository.save(user);
// Send verification email event
kafkaTemplate.send("user-events", "user.registered", 
new UserRegisteredEvent(savedUser.getId(), savedUser.getEmail()));
return mapToUserResponse(savedUser);
}
public AuthenticationResponse authenticate(LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new AuthenticationException("Invalid credentials"));
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new AuthenticationException("Invalid credentials");
}
if (!user.isEnabled()) {
throw new AuthenticationException("Account is disabled");
}
String token = tokenProvider.generateToken(user);
return new AuthenticationResponse(token, mapToUserResponse(user));
}
public UserResponse updateProfile(Long userId, UserUpdateRequest request) {
User user = getUserById(userId);
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setAddress(request.getAddress());
User updatedUser = userRepository.save(user);
// Publish profile update event
kafkaTemplate.send("user-events", "user.profile.updated",
new UserProfileUpdatedEvent(userId));
return mapToUserResponse(updatedUser);
}
public void updatePreferences(Long userId, Map<String, String> preferences) {
User user = getUserById(userId);
user.setPreferences(preferences);
userRepository.save(user);
}
private User getUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
}
private UserResponse mapToUserResponse(User user) {
return new UserResponse(
user.getId(),
user.getEmail(),
user.getFirstName(),
user.getLastName(),
user.getRole(),
user.getAddress(),
user.getPreferences()
);
}
}

3. User Controller

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/register")
public ResponseEntity<ApiResponse<UserResponse>> register(
@Valid @RequestBody UserRegistrationRequest request) {
UserResponse userResponse = userService.registerUser(request);
return ResponseEntity.ok(ApiResponse.success(userResponse));
}
@PostMapping("/login")
public ResponseEntity<ApiResponse<AuthenticationResponse>> login(
@Valid @RequestBody LoginRequest request) {
AuthenticationResponse authResponse = userService.authenticate(request);
return ResponseEntity.ok(ApiResponse.success(authResponse));
}
@PutMapping("/profile")
public ResponseEntity<ApiResponse<UserResponse>> updateProfile(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@Valid @RequestBody UserUpdateRequest request) {
UserResponse userResponse = userService.updateProfile(userPrincipal.getId(), request);
return ResponseEntity.ok(ApiResponse.success(userResponse));
}
@PutMapping("/preferences")
public ResponseEntity<ApiResponse<Void>> updatePreferences(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody Map<String, String> preferences) {
userService.updatePreferences(userPrincipal.getId(), preferences);
return ResponseEntity.ok(ApiResponse.success(null));
}
@GetMapping("/profile")
public ResponseEntity<ApiResponse<UserResponse>> getProfile(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
UserResponse userResponse = userService.getUserProfile(userPrincipal.getId());
return ResponseEntity.ok(ApiResponse.success(userResponse));
}
}

Product Service Implementation

1. Product Domain Models

@Entity
@Table(name = "products")
public class Product extends BaseEntity {
@NotBlank
private String name;
@NotBlank
@Column(columnDefinition = "TEXT")
private String description;
@DecimalMin("0.0")
private BigDecimal price;
@DecimalMin("0.0")
private BigDecimal salePrice;
@Min(0)
private Integer stockQuantity;
@Min(0)
private Integer reservedQuantity;
private String sku;
private String imageUrl;
@Enumerated(EnumType.STRING)
private ProductStatus status = ProductStatus.ACTIVE;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seller_id")
private User seller;
@ElementCollection
@CollectionTable(name = "product_attributes", joinColumns = @JoinColumn(name = "product_id"))
private Map<String, String> attributes = new HashMap<>();
// Constructors, getters, setters
public boolean isInStock() {
return stockQuantity > 0;
}
public boolean isOnSale() {
return salePrice != null && salePrice.compareTo(BigDecimal.ZERO) > 0;
}
public BigDecimal getCurrentPrice() {
return isOnSale() ? salePrice : price;
}
public void reserveQuantity(int quantity) {
if (quantity > getAvailableQuantity()) {
throw new InsufficientStockException("Insufficient stock available");
}
this.reservedQuantity += quantity;
}
public void releaseQuantity(int quantity) {
this.reservedQuantity = Math.max(0, this.reservedQuantity - quantity);
}
public void reduceStock(int quantity) {
if (quantity > getAvailableQuantity()) {
throw new InsufficientStockException("Insufficient stock available");
}
this.stockQuantity -= quantity;
this.reservedQuantity = Math.max(0, this.reservedQuantity - quantity);
}
public int getAvailableQuantity() {
return stockQuantity - reservedQuantity;
}
}
@Entity
@Table(name = "categories")
public class Category extends BaseEntity {
@NotBlank
private String name;
private String description;
private String imageUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> children = new ArrayList<>();
// Constructors, getters, setters
}
@Entity
@Table(name = "product_reviews")
public class ProductReview extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Min(1)
@Max(5)
private Integer rating;
private String title;
private String comment;
private boolean verifiedPurchase;
// Constructors, getters, setters
}

2. Product Service Implementation

@Service
@Transactional
public class ProductService {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
private final ProductSearchRepository productSearchRepository;
private final KafkaTemplate<String, Object> kafkaTemplate;
public ProductService(ProductRepository productRepository, 
CategoryRepository categoryRepository,
ProductSearchRepository productSearchRepository,
KafkaTemplate<String, Object> kafkaTemplate) {
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
this.productSearchRepository = productSearchRepository;
this.kafkaTemplate = kafkaTemplate;
}
public ProductResponse createProduct(ProductCreateRequest request, Long sellerId) {
Category category = categoryRepository.findById(request.getCategoryId())
.orElseThrow(() -> new ResourceNotFoundException("Category not found"));
Product product = new Product();
product.setName(request.getName());
product.setDescription(request.getDescription());
product.setPrice(request.getPrice());
product.setSalePrice(request.getSalePrice());
product.setStockQuantity(request.getStockQuantity());
product.setSku(generateSku(request.getName()));
product.setImageUrl(request.getImageUrl());
product.setCategory(category);
product.setSeller(new User(sellerId)); // Reference only
Product savedProduct = productRepository.save(product);
// Index product for search
productSearchRepository.indexProduct(savedProduct);
// Publish product created event
kafkaTemplate.send("product-events", "product.created",
new ProductCreatedEvent(savedProduct.getId(), savedProduct.getName()));
return mapToProductResponse(savedProduct);
}
public PaginatedResponse<ProductResponse> searchProducts(ProductSearchRequest request) {
Pageable pageable = PageRequest.of(request.getPage(), request.getSize(),
Sort.by(Sort.Direction.fromString(request.getSortDirection()), request.getSortBy()));
Specification<Product> spec = buildSearchSpecification(request);
Page<Product> products = productRepository.findAll(spec, pageable);
List<ProductResponse> productResponses = products.getContent().stream()
.map(this::mapToProductResponse)
.collect(Collectors.toList());
return new PaginatedResponse<>(
productResponses,
products.getNumber(),
products.getSize(),
products.getTotalElements(),
products.getTotalPages(),
products.isLast()
);
}
public ProductResponse getProductById(Long productId) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
return mapToProductResponse(product);
}
public void updateStock(Long productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
product.setStockQuantity(quantity);
productRepository.save(product);
// Update search index
productSearchRepository.indexProduct(product);
// Publish stock update event
kafkaTemplate.send("product-events", "product.stock.updated",
new ProductStockUpdatedEvent(productId, quantity));
}
public void reserveProduct(Long productId, int quantity) {
Product product = productRepository.findByIdWithLock(productId)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
product.reserveQuantity(quantity);
productRepository.save(product);
}
public void releaseProductReservation(Long productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
product.releaseQuantity(quantity);
productRepository.save(product);
}
private Specification<Product> buildSearchSpecification(ProductSearchRequest request) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
// Name search
if (StringUtils.isNotBlank(request.getQuery())) {
predicates.add(criteriaBuilder.like(
criteriaBuilder.lower(root.get("name")),
"%" + request.getQuery().toLowerCase() + "%"
));
}
// Category filter
if (request.getCategoryId() != null) {
predicates.add(criteriaBuilder.equal(root.get("category").get("id"), request.getCategoryId()));
}
// Price range
if (request.getMinPrice() != null) {
predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("price"), request.getMinPrice()));
}
if (request.getMaxPrice() != null) {
predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("price"), request.getMaxPrice()));
}
// In stock filter
if (request.getInStock() != null && request.getInStock()) {
predicates.add(criteriaBuilder.greaterThan(root.get("stockQuantity"), 0));
}
// Status filter
predicates.add(criteriaBuilder.equal(root.get("status"), ProductStatus.ACTIVE));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
private String generateSku(String productName) {
String base = productName.replaceAll("[^a-zA-Z0-9]", "").toUpperCase();
String timestamp = String.valueOf(System.currentTimeMillis());
return base.substring(0, Math.min(base.length(), 8)) + "-" + timestamp.substring(timestamp.length() - 6);
}
private ProductResponse mapToProductResponse(Product product) {
return new ProductResponse(
product.getId(),
product.getName(),
product.getDescription(),
product.getPrice(),
product.getSalePrice(),
product.getCurrentPrice(),
product.getStockQuantity(),
product.getAvailableQuantity(),
product.getSku(),
product.getImageUrl(),
product.getStatus(),
product.getCategory().getName(),
product.getSeller().getId(),
product.getAttributes()
);
}
}

3. Elasticsearch Integration

@Document(indexName = "products")
public class ProductDocument {
@Id
private Long id;
private String name;
private String description;
private BigDecimal price;
private BigDecimal salePrice;
private String category;
private String imageUrl;
private Integer stockQuantity;
private String status;
private Map<String, String> attributes;
private Instant createdAt;
// Constructors, getters, setters
}
@Repository
public interface ProductSearchRepository extends ElasticsearchRepository<ProductDocument, Long> {
Page<ProductDocument> findByNameContainingOrDescriptionContaining(
String name, String description, Pageable pageable);
Page<ProductDocument> findByCategoryAndPriceBetween(
String category, BigDecimal minPrice, BigDecimal maxPrice, Pageable pageable);
@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"name\", \"description\", \"attributes.*\"]}}")
Page<ProductDocument> searchProducts(String query, Pageable pageable);
default void indexProduct(Product product) {
ProductDocument document = new ProductDocument(
product.getId(),
product.getName(),
product.getDescription(),
product.getPrice(),
product.getSalePrice(),
product.getCategory().getName(),
product.getImageUrl(),
product.getStockQuantity(),
product.getStatus().name(),
product.getAttributes(),
product.getCreatedAt()
);
save(document);
}
}

Order Service Implementation

1. Order Domain Models

@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@Column(unique = true)
private String orderNumber;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Enumerated(EnumType.STRING)
private OrderStatus status = OrderStatus.PENDING;
@Embedded
private Address shippingAddress;
@Embedded
private Address billingAddress;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@OneToOne(mappedBy = "order", cascade = CascadeType.ALL)
private Payment payment;
private BigDecimal subtotal;
private BigDecimal taxAmount;
private BigDecimal shippingCost;
private BigDecimal discountAmount;
private BigDecimal totalAmount;
private String shippingMethod;
private String trackingNumber;
// Constructors, getters, setters
public void calculateTotals() {
this.subtotal = items.stream()
.map(OrderItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
this.taxAmount = subtotal.multiply(new BigDecimal("0.1")); // 10% tax example
this.shippingCost = calculateShippingCost();
this.discountAmount = calculateDiscount();
this.totalAmount = subtotal
.add(taxAmount)
.add(shippingCost)
.subtract(discountAmount);
}
private BigDecimal calculateShippingCost() {
// Complex shipping calculation logic
return new BigDecimal("9.99");
}
private BigDecimal calculateDiscount() {
// Discount calculation logic
return BigDecimal.ZERO;
}
}
@Entity
@Table(name = "order_items")
public class OrderItem extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
private String productName;
private String productSku;
private BigDecimal unitPrice;
private Integer quantity;
private BigDecimal totalPrice;
// Constructors, getters, setters
public void calculateTotalPrice() {
this.totalPrice = unitPrice.multiply(new BigDecimal(quantity));
}
}
@Entity
@Table(name = "payments")
public class Payment extends BaseEntity {
@OneToOne
@JoinColumn(name = "order_id")
private Order order;
@Enumerated(EnumType.STRING)
private PaymentStatus status = PaymentStatus.PENDING;
@Enumerated(EnumType.STRING)
private PaymentMethod method;
private String transactionId;
private BigDecimal amount;
private String currency = "USD";
private String paymentGateway;
private String gatewayTransactionId;
private String gatewayResponse;
// Constructors, getters, setters
}

2. Order Service Implementation

@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ProductServiceClient productServiceClient;
private final PaymentServiceClient paymentServiceClient;
private final KafkaTemplate<String, Object> kafkaTemplate;
private final RedisTemplate<String, Object> redisTemplate;
public OrderService(OrderRepository orderRepository,
ProductServiceClient productServiceClient,
PaymentServiceClient paymentServiceClient,
KafkaTemplate<String, Object> kafkaTemplate,
RedisTemplate<String, Object> redisTemplate) {
this.orderRepository = orderRepository;
this.productServiceClient = productServiceClient;
this.paymentServiceClient = paymentServiceClient;
this.kafkaTemplate = kafkaTemplate;
this.redisTemplate = redisTemplate;
}
public OrderResponse createOrder(OrderCreateRequest request, Long userId) {
// Validate and reserve products
List<OrderItem> orderItems = validateAndCreateOrderItems(request.getItems());
// Create order
Order order = new Order();
order.setOrderNumber(generateOrderNumber());
order.setUser(new User(userId));
order.setShippingAddress(request.getShippingAddress());
order.setBillingAddress(request.getBillingAddress());
order.setShippingMethod(request.getShippingMethod());
order.setItems(orderItems);
order.calculateTotals();
Order savedOrder = orderRepository.save(order);
// Create payment record
PaymentCreateRequest paymentRequest = new PaymentCreateRequest(
savedOrder.getId(),
savedOrder.getTotalAmount(),
request.getPaymentMethod()
);
PaymentResponse paymentResponse = paymentServiceClient.createPayment(paymentRequest);
// Publish order created event
kafkaTemplate.send("order-events", "order.created",
new OrderCreatedEvent(savedOrder.getId(), savedOrder.getOrderNumber(), userId));
return mapToOrderResponse(savedOrder, paymentResponse);
}
public OrderResponse processOrderPayment(Long orderId, PaymentProcessRequest request) {
Order order = getOrderById(orderId);
// Process payment through payment service
PaymentResponse paymentResponse = paymentServiceClient.processPayment(
order.getPayment().getId(), request);
if (paymentResponse.getStatus() == PaymentStatus.COMPLETED) {
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.save(order);
// Update product stock
updateProductStock(order);
// Publish order confirmed event
kafkaTemplate.send("order-events", "order.confirmed",
new OrderConfirmedEvent(orderId, order.getOrderNumber()));
// Send confirmation email
kafkaTemplate.send("notification-events", "order.confirmation",
new OrderConfirmationEvent(order.getUser().getId(), orderId));
} else {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
// Release reserved products
releaseReservedProducts(order);
}
return mapToOrderResponse(order, paymentResponse);
}
public void cancelOrder(Long orderId) {
Order order = getOrderById(orderId);
if (order.getStatus().canBeCancelled()) {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
// Release reserved products
releaseReservedProducts(order);
// Refund payment if already processed
if (order.getPayment().getStatus() == PaymentStatus.COMPLETED) {
paymentServiceClient.refundPayment(order.getPayment().getId());
}
// Publish order cancelled event
kafkaTemplate.send("order-events", "order.cancelled",
new OrderCancelledEvent(orderId, order.getOrderNumber()));
}
}
public OrderResponse updateOrderStatus(Long orderId, OrderStatus status) {
Order order = getOrderById(orderId);
order.setStatus(status);
Order updatedOrder = orderRepository.save(order);
// Publish status update event
kafkaTemplate.send("order-events", "order.status.updated",
new OrderStatusUpdatedEvent(orderId, status));
return mapToOrderResponse(updatedOrder, null);
}
private List<OrderItem> validateAndCreateOrderItems(List<OrderItemRequest> itemRequests) {
List<OrderItem> orderItems = new ArrayList<>();
for (OrderItemRequest itemRequest : itemRequests) {
// Check product availability through product service
ProductResponse product = productServiceClient.getProduct(itemRequest.getProductId());
if (product.getAvailableQuantity() < itemRequest.getQuantity()) {
throw new InsufficientStockException(
"Insufficient stock for product: " + product.getName());
}
// Reserve product
productServiceClient.reserveProduct(itemRequest.getProductId(), itemRequest.getQuantity());
OrderItem orderItem = new OrderItem();
orderItem.setProduct(new Product(itemRequest.getProductId()));
orderItem.setProductName(product.getName());
orderItem.setProductSku(product.getSku());
orderItem.setUnitPrice(product.getCurrentPrice());
orderItem.setQuantity(itemRequest.getQuantity());
orderItem.calculateTotalPrice();
orderItems.add(orderItem);
}
return orderItems;
}
private void updateProductStock(Order order) {
for (OrderItem item : order.getItems()) {
productServiceClient.reduceStock(item.getProduct().getId(), item.getQuantity());
}
}
private void releaseReservedProducts(Order order) {
for (OrderItem item : order.getItems()) {
try {
productServiceClient.releaseProductReservation(
item.getProduct().getId(), item.getQuantity());
} catch (Exception e) {
// Log error but continue with other items
log.error("Failed to release reservation for product: {}", item.getProduct().getId(), e);
}
}
}
private String generateOrderNumber() {
String timestamp = String.valueOf(System.currentTimeMillis());
String random = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 9999));
return "ORD-" + timestamp + "-" + random;
}
private Order getOrderById(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
}
private OrderResponse mapToOrderResponse(Order order, PaymentResponse paymentResponse) {
return new OrderResponse(
order.getId(),
order.getOrderNumber(),
order.getStatus(),
order.getUser().getId(),
order.getShippingAddress(),
order.getBillingAddress(),
order.getItems().stream().map(this::mapToOrderItemResponse).collect(Collectors.toList()),
order.getSubtotal(),
order.getTaxAmount(),
order.getShippingCost(),
order.getDiscountAmount(),
order.getTotalAmount(),
order.getShippingMethod(),
order.getTrackingNumber(),
paymentResponse,
order.getCreatedAt()
);
}
}

API Gateway Configuration

1. Spring Cloud Gateway Configuration

# application.yml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
- name: JwtAuthentication
- StripPrefix=1
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
- StripPrefix=1
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 50
- name: JwtAuthentication
- StripPrefix=1
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/api/payments/**
filters:
- name: JwtAuthentication
- StripPrefix=1
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "https://your-frontend.com"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true

2. JWT Authentication Filter

@Component
public class JwtAuthenticationFilter implements GlobalFilter {
private final JwtTokenProvider tokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// Skip authentication for public endpoints
if (isPublicEndpoint(path)) {
return chain.filter(exchange);
}
String token = extractToken(exchange.getRequest());
if (token == null || !tokenProvider.validateToken(token)) {
return onError(exchange, "Invalid or missing token", HttpStatus.UNAUTHORIZED);
}
// Add user info to headers for downstream services
String username = tokenProvider.getUsernameFromToken(token);
Long userId = tokenProvider.getUserIdFromToken(token);
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Id", userId.toString())
.header("X-User-Name", username)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
private boolean isPublicEndpoint(String path) {
return path.startsWith("/api/users/register") ||
path.startsWith("/api/users/login") ||
path.startsWith("/api/products/search") ||
path.startsWith("/api/public/");
}
private String extractToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus status) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(status);
byte[] bytes = ("{\"error\": \"" + err + "\"}").getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return response.writeWith(Mono.just(buffer));
}
}

Event-Driven Architecture with Kafka

1. Event Definitions

public abstract class DomainEvent {
private String eventId;
private String eventType;
private Instant timestamp;
private String source;
protected DomainEvent(String eventType) {
this.eventId = UUID.randomUUID().toString();
this.eventType = eventType;
this.timestamp = Instant.now();
this.source = "ecommerce-platform";
}
// Getters and setters
}
public class OrderCreatedEvent extends DomainEvent {
private Long orderId;
private String orderNumber;
private Long userId;
private BigDecimal totalAmount;
public OrderCreatedEvent(Long orderId, String orderNumber, Long userId) {
super("order.created");
this.orderId = orderId;
this.orderNumber = orderNumber;
this.userId = userId;
}
// Getters and setters
}
public class PaymentProcessedEvent extends DomainEvent {
private Long paymentId;
private Long orderId;
private PaymentStatus status;
private BigDecimal amount;
public PaymentProcessedEvent(Long paymentId, Long orderId, PaymentStatus status, BigDecimal amount) {
super("payment.processed");
this.paymentId = paymentId;
this.orderId = orderId;
this.status = status;
this.amount = amount;
}
// Getters and setters
}

2. Event Consumers

@Component
public class OrderEventConsumer {
private final NotificationService notificationService;
private final ShippingService shippingService;
private final ObjectMapper objectMapper;
public OrderEventConsumer(NotificationService notificationService,
ShippingService shippingService,
ObjectMapper objectMapper) {
this.notificationService = notificationService;
this.shippingService = shippingService;
this.objectMapper = objectMapper;
}
@KafkaListener(topics = "order-events")
public void handleOrderEvent(String message) {
try {
JsonNode jsonNode = objectMapper.readTree(message);
String eventType = jsonNode.get("eventType").asText();
switch (eventType) {
case "order.confirmed":
OrderConfirmedEvent confirmedEvent = objectMapper.treeToValue(jsonNode, OrderConfirmedEvent.class);
handleOrderConfirmed(confirmedEvent);
break;
case "order.shipped":
OrderShippedEvent shippedEvent = objectMapper.treeToValue(jsonNode, OrderShippedEvent.class);
handleOrderShipped(shippedEvent);
break;
default:
log.warn("Unknown order event type: {}", eventType);
}
} catch (Exception e) {
log.error("Error processing order event: {}", message, e);
}
}
private void handleOrderConfirmed(OrderConfirmedEvent event) {
// Send order confirmation email
notificationService.sendOrderConfirmation(event.getOrderId(), event.getUserId());
// Initiate shipping process
shippingService.createShippingLabel(event.getOrderId());
}
private void handleOrderShipped(OrderShippedEvent event) {
// Send shipping notification
notificationService.sendShippingNotification(event.getOrderId(), event.getTrackingNumber());
}
}

Monitoring and Observability

1. Application Monitoring Configuration

# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true
# Custom metrics
custom:
metrics:
enabled: true
prefix: ecommerce

2. Custom Metrics

@Component
public class EcommerceMetrics {
private final MeterRegistry meterRegistry;
private final Counter orderCreatedCounter;
private final Counter paymentProcessedCounter;
private final DistributionSummary orderValueSummary;
public EcommerceMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.orderCreatedCounter = Counter.builder("ecommerce.orders.created")
.description("Number of orders created")
.register(meterRegistry);
this.paymentProcessedCounter = Counter.builder("ecommerce.payments.processed")
.description("Number of payments processed")
.tag("status", "unknown")
.register(meterRegistry);
this.orderValueSummary = DistributionSummary.builder("ecommerce.orders.value")
.description("Order value distribution")
.baseUnit("USD")
.register(meterRegistry);
}
public void recordOrderCreated(Order order) {
orderCreatedCounter.increment();
orderValueSummary.record(order.getTotalAmount().doubleValue());
}
public void recordPaymentProcessed(Payment payment) {
paymentProcessedCounter.increment();
}
}

Deployment and Configuration

1. Docker Compose for Development

version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: ecommerce
POSTGRES_USER: admin
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
zookeeper:
image: confluentinc/cp-zookeeper:7.3.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
kafka:
image: confluentinc/cp-kafka:7.3.0
depends_on:
- zookeeper
environment:
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
elasticsearch:
image: elasticsearch:8.5.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
postgres_data:

2. Kubernetes Deployment Example

# k8s/user-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: ecommerce/user-service:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 15
periodSeconds: 5

Conclusion

Building a scalable e-commerce platform in Java requires careful consideration of architecture, data models, and integration patterns. The microservices approach with Spring Boot provides flexibility and scalability, while technologies like Kafka enable robust event-driven communication. Key considerations include:

  1. Domain-Driven Design: Properly modeled entities and aggregates
  2. Event Sourcing: For reliable state changes and audit trails
  3. CQRS: Separate read and write models for performance
  4. Circuit Breakers: For resilient service communication
  5. Monitoring: Comprehensive observability for production operations
  6. Security: End-to-end security with JWT and proper authorization

This architecture provides a solid foundation that can scale from startup to enterprise-level e-commerce operations while maintaining development velocity and operational reliability.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper