Comprehensive Hands-On: OrderFlow Project Development
Project development is the process of turning design into code—advancing in a modular fashion, with each step verifiable and continuous integration to prevent backlogs.
1. What You'll Learn
- User Module: Registration / Login, JWT Issuance / Role-Based Access Control
- Product Module: CRUD / Redis Caching / Caffeine Second-Level Caching
- Order Module: Order Placement / Cancellation / Automatic Cancellation Due to Timeout / Inventory Deduction Transactions
- Payment Module: Simulated Payments / Callback Handling / Idempotency Guarantee
- Global Features: Exception Handling / Request Logging / Parameter Validation / API Documentation (SpringDoc)
2. A True Story of a Full-Stack Developer
(1) Pain Point: The Gap Between Design and Code
Alice has completed the system design for OrderFlow, but she feels overwhelmed by the sheer volume of code—5 tables, 20 APIs, and 4 modules. Should she start by writing the database code or the API code? How should the modules depend on each other? How can she ensure that each step runs and passes validation?
(2) Solutions for Modular Development
Proceed step by step on a module-by-module basis, with each module verifiable independently:
graph TD
A["User Module<br/>Auth + JWT"] --> B["Product Module<br/>CRUD + Cache"]
B --> C["Order Module<br/>Transaction + Schedule"]
C --> D["Payment Module<br/>Callback + Idempotent"]
D --> E["Cross-cutting<br/>Exception + Logging + Doc"]
(3) Revenue
Alice developed the project module by module, running tests to verify each module as it was completed. She finished the entire codebase within two weeks, avoiding the "big bang" issues associated with a one-time integration.
3. User Module
(1) ▶ Example: User Entity and Repository
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false, name = "password_hash")
private String passwordHash;
@Column(nullable = false, length = 20)
private String role = "CUSTOMER";
@CreationTimestamp
private Instant createdAt;
protected User() {}
public User(String username, String email, String passwordHash, String role) {
this.username = username;
this.email = email;
this.passwordHash = passwordHash;
this.role = role;
}
// getters
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
Output:
// Execution Successful
(2) ▶ Example: Registration and Login
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtEncoder jwtEncoder;
public AuthService(UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtEncoder jwtEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtEncoder = jwtEncoder;
}
@Transactional
public UserResponse register(RegisterRequest request) {
if (userRepository.existsByUsername(request.username())) {
throw new BusinessException("USERNAME_EXISTS", "Username already taken");
}
User user = new User(
request.username(),
request.email(),
passwordEncoder.encode(request.password()),
"CUSTOMER"
);
return UserResponse.from(userRepository.save(user));
}
public Map<String, String> login(LoginRequest request) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(), request.password()));
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("orderflow").subject(auth.getName())
.issuedAt(now).expiresAt(now.plus(2, ChronoUnit.HOURS))
.claim("role", extractRole(auth))
.build();
String token = jwtEncoder
.encode(JwtEncoderParameters.from(claims)).getTokenValue();
return Map.of("accessToken", token);
}
}
Output:
// Execution Successful
4. Product Module
(1) ▶ Example: ProductService with Caching
@Service
public class ProductService {
private final ProductRepository productRepository;
private final ProductSearchRepository searchRepository; // Redis
@Cacheable(value = "products", key = "#id")
public ProductResponse getProduct(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
return ProductResponse.from(product);
}
@Cacheable(value = "product-list",
key = "#keyword + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
public PagedResponse<ProductResponse> searchProducts(
String keyword, Pageable pageable) {
Page<Product> page = productRepository
.findByNameContaining(keyword, pageable);
return PagedResponse.from(page.map(ProductResponse::from));
}
@CacheEvict(value = "product-list", allEntries = true)
@CachePut(value = "products", key = "#result.id()")
public ProductResponse createProduct(CreateProductRequest request) {
Product product = new Product(
request.name(), request.sku(),
request.price(), request.stock(), request.category());
return ProductResponse.from(productRepository.save(product));
}
@CachePut(value = "products", key = "#result.id()")
@CacheEvict(value = "product-list", allEntries = true)
public ProductResponse updateProduct(Long id, UpdateProductRequest request) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
product.setName(request.name());
product.setPrice(request.price());
product.setStock(request.stock());
return ProductResponse.from(productRepository.save(product));
}
@Caching(evict = {
@CacheEvict(value = "products", key = "#id"),
@CacheEvict(value = "product-list", allEntries = true)
})
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}
Output:
// Execution Successful
5. Order Module
(1) ▶ Example: OrderService Order Transaction
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final UserRepository userRepository;
private final OrderEventPublisher eventPublisher;
@Transactional(rollbackFor = Exception.class)
public OrderResponse createOrder(String username, CreateOrderRequest request) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User", 0L));
Product product = productRepository.findById(request.productId())
.orElseThrow(() -> new ResourceNotFoundException("Product", request.productId()));
if (product.getStock() < request.quantity()) {
throw new InsufficientStockException(
product.getId(), product.getStock(), request.quantity());
}
product.deductStock(request.quantity());
Order order = new Order(user, product, request.quantity());
Order saved = orderRepository.save(order);
eventPublisher.publishOrderCreated(saved.getId());
return OrderResponse.from(saved);
}
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(String username, Long orderId) {
Order order = orderRepository.findByIdWithItems(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", orderId));
if (!"PENDING".equals(order.getStatus())) {
throw new OrderStateException(orderId, order.getStatus(), "CANCELLED");
}
order.getItems().forEach(item ->
item.getProduct().addStock(item.getQuantity()));
order.setStatus("CANCELLED");
eventPublisher.publishOrderCancelled(orderId);
}
@Scheduled(fixedRateString = "${orderflow.order.expiry-check-interval:300000}")
@Transactional
public void cancelExpiredOrders() {
List<Order> expired = orderRepository
.findByStatusAndCreatedAtBefore("PENDING",
Instant.now().minus(30, ChronoUnit.MINUTES));
expired.forEach(order -> {
order.getItems().forEach(item ->
item.getProduct().addStock(item.getQuantity()));
order.setStatus("CANCELLED");
});
if (!expired.isEmpty()) {
log.info("Auto-cancelled {} expired orders", expired.size());
}
}
}
Output:
// Execution Successful
6. Payment Module
(1) ▶ Example: PaymentService with Idempotency
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final OrderRepository orderRepository;
private final StringRedisTemplate redis;
@Transactional(rollbackFor = Exception.class)
public PaymentResponse initiatePayment(String username, CreatePaymentRequest request) {
Order order = orderRepository.findById(request.orderId())
.orElseThrow(() -> new ResourceNotFoundException("Order", request.orderId()));
if (!"PENDING".equals(order.getStatus())) {
throw new OrderStateException(order.getId(), order.getStatus(), "PAYMENT");
}
// Idempotency check: same transaction_id should not create duplicate payment
String idempotencyKey = "payment:idempotent:" + request.orderId();
Boolean isNew = redis.opsForValue()
.setIfAbsent(idempotencyKey, "1", Duration.ofMinutes(10));
if (Boolean.FALSE.equals(isNew)) {
throw new BusinessException("DUPLICATE_PAYMENT",
"Payment already initiated for order " + request.orderId());
}
Payment payment = new Payment(order, request.method(), order.getTotalAmount());
payment.setTransactionId(generateTransactionId());
return PaymentResponse.from(paymentRepository.save(payment));
}
@Transactional(rollbackFor = Exception.class)
public void handlePaymentCallback(String transactionId, String status) {
Payment payment = paymentRepository.findByTransactionId(transactionId)
.orElseThrow(() -> new ResourceNotFoundException("Payment", 0L));
if ("COMPLETED".equals(status) && "PENDING".equals(payment.getStatus())) {
payment.setStatus("COMPLETED");
payment.setPaidAt(Instant.now());
payment.getOrder().setStatus("PAID");
} else if ("FAILED".equals(status)) {
payment.setStatus("FAILED");
}
}
private String generateTransactionId() {
return "TXN-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
}
}
Output:
// Execution Successful
7. Global Properties
(1) ▶ Example: SpringDoc API Documentation Configuration
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI orderFlowOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("OrderFlow API")
.version("1.0.0")
.description("E-commerce order management system API"))
.addSecurityItem(new SecurityRequirement().addList("Bearer Auth"))
.schemaRequirement("Bearer Auth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT"));
}
}
Output:
// Execution Successful
| Global Properties | Implementation | Scope |
|---|---|---|
| Exception Handling | @RestControllerAdvice | Global Standardized Error Format |
| Request Logs | HandlerInterceptor | All Inbound Request Logs |
| Parameter Validation | Bean Validation | All Request DTOs |
| API Documentation | SpringDoc OpenAPI | Automatically Generate Swagger UI |
| Security Authentication | Spring Security + JWT | All Protected Interfaces |
8. Comprehensive Example: Complete Project Structure for OrderFlow
orderflow-service/
├── src/main/java/com/orderflow/
│ ├── OrderFlowApplication.java
│ ├── config/
│ │ ├── SecurityConfig.java # JWT + Role-based auth
│ │ ├── CacheConfig.java # Caffeine + Redis
│ │ ├── AsyncConfig.java # Thread pool + Scheduling
│ │ └── OpenApiConfig.java # API documentation
│ ├── controller/
│ │ ├── AuthController.java # Login / Register
│ │ ├── ProductController.java # Product CRUD
│ │ ├── OrderController.java # Order management
│ │ └── PaymentController.java # Payment processing
│ ├── service/
│ │ ├── AuthService.java
│ │ ├── ProductService.java
│ │ ├── OrderService.java
│ │ ├── PaymentService.java
│ │ └── NotificationService.java # @Async notifications
│ ├── repository/
│ │ ├── UserRepository.java
│ │ ├── ProductRepository.java
│ │ ├── OrderRepository.java
│ │ └── PaymentRepository.java
│ ├── model/
│ │ ├── User.java
│ │ ├── Product.java
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ └── Payment.java
│ ├── dto/
│ │ ├── request/ # CreateOrderRequest, etc.
│ │ └── response/ # OrderResponse, etc.
│ ├── exception/
│ │ ├── BusinessException.java
│ │ ├── ResourceNotFoundException.java
│ │ ├── GlobalExceptionHandler.java
│ │ └── ErrorResponse.java
│ ├── security/
│ │ ├── JwtConfig.java # RSA key pair
│ │ ├── CustomUserDetailsService.java
│ │ └── OrderSecurity.java # @PreAuthorize helper
│ └── metrics/
│ └── OrderMetrics.java # Custom Micrometer metrics
├── src/main/resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── db/migration/ # Flyway scripts
│ ├── V1__init_schema.sql
│ └── V2__add_indexes.sql
├── Dockerfile
├── docker-compose.yml
└── pom.xml
❓ FAQ
@Schema annotation to DTOs to provide additional explanations.V{version}__{description}.sql, such as V1__init_schema.sql and V2__add_indexes.sql. Version numbers must be sequential, and scripts that have already been executed must not be modified.📖 Summary
- User Module: BCrypt password hashing, JWT issuance, role-based access control
- Product Module: @Cacheable for two-level caching, @CachePut for synchronous updates, @CacheEvict for eviction
- Order Module: @Transactional for inventory deduction upon order placement, @Scheduled for automatic cancellation upon timeout, and Pub/Sub event notifications
- Payment Module: Redis SETNX idempotency, state machine guarantees, callback handling
- Global Features: @RestControllerAdvice for unified exception handling, SpringDoc API documentation, and Bean Validation
📝 Exercises
-
Basic Exercise (Difficulty: ⭐): Implement complete code for user registration/login and product CRUD operations, ensuring that JWT authentication and caching are enabled.
-
Advanced Exercise (Difficulty: ⭐⭐): Implement the order module (placing an order, canceling an order, and automatic cancellation due to timeout) and the payment module (initiating payment and idempotent callbacks), and use Postman to perform end-to-end business process testing.
-
Challenge (Difficulty: ⭐⭐⭐): Add SpringDoc API documentation to OrderFlow and configure Swagger UI for interactive testing; add Flyway database migration scripts; write integration tests for the core workflow using TestContainers.



