Phase 2 Comprehensive Exercise: Refining the Core Business Functions of OrderFlow
Integration is the litmus test of understanding—combining JPA, transactions, validation, exception handling, and security into a single, fully functional system.
1. What You'll Learn
- Implement multi-table joins and cascading operations between Order, OrderItem, and Product
- Improve the order placement process: Verify inventory → Deduct inventory → Create order → Transaction guarantee
- Unified exception handling covers all business scenarios
- Control API permissions by role (CUSTOMER / ADMIN)
- Use the H2 console and Postman to perform end-to-end business process testing
2. A True Story of a Full-Stack Developer
(1) Pain Point: Modules Operating in Isolation
Alice has completed her studies on the five modules—JPA, Service, Validation, Exception Handling, and Security—but they all exist in isolation. When actually placing an order, she needs to link together the entire process: "validate input → check inventory → deduct inventory → create order → ensure transaction integrity → enforce access control → handle exceptions." She's not sure how the various modules work together.
(2) Solutions to the Comprehensive Exercises
This lesson integrates all the modules into a complete order placement process, simulating a real-world business scenario.
(3) Revenue
With the integration by Alice complete, OrderFlow now has a fully closed-loop business capability—from user login to placing orders, querying, and canceling, each step is protected by validation, transactions, permissions, and exception handling.
3. Data Model Integration
(1) Complete ER Model
erDiagram
PRODUCT ||--o{ ORDER_ITEM : "included in"
ORDER ||--o{ ORDER_ITEM : "contains"
USER ||--o{ ORDER : "places"
PRODUCT {
bigint id PK
varchar name
decimal price
int stock
varchar sku
}
ORDER {
bigint id PK
bigint user_id FK
varchar status
decimal total_amount
timestamp created_at
}
ORDER_ITEM {
bigint id PK
bigint order_id FK
bigint product_id FK
int quantity
decimal unit_price
}
USER {
bigint id PK
varchar username
varchar password
varchar email
varchar role
}
(1) ▶ Example: User Entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String role;
protected User() {}
public User(String username, String password, String email, String role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
// getters
}
Output:
// Execution Successful
4. Ordering Process Integration
(1) Complete Order Timeline
sequenceDiagram
participant C as Client
participant Ctrl as OrderController
participant Val as Validation
participant Svc as OrderService
participant Tx as Transaction
participant Repo as Repository
C->>Ctrl: POST /api/v1/orders
Ctrl->>Val: @Valid CreateOrderRequest
alt Validation fails
Val-->>C: 400 VALIDATION_ERROR
end
Ctrl->>Svc: createOrder(request)
Svc->>Tx: Begin Transaction
Svc->>Repo: findProductById
alt Product not found
Svc-->>C: 404 RESOURCE_NOT_FOUND
end
Svc->>Repo: deductStock (check quantity)
alt Insufficient stock
Tx->>Tx: Rollback
Svc-->>C: 409 INSUFFICIENT_STOCK
end
Svc->>Repo: save Order + OrderItems
Tx->>Tx: Commit
Svc-->>Ctrl: Order created
Ctrl-->>C: 201 Created
(1) ▶ Example: Complete OrderController
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request,
@AuthenticationPrincipal UserDetails userDetails) {
OrderResponse order = orderService.createOrder(
request, userDetails.getUsername());
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id, authentication)")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderService.getOrder(id));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
orderService.cancelOrder(id);
return ResponseEntity.noContent().build();
}
}
Output:
// Execution Successful
5. Transaction Integration at the Service Layer
(1) ▶ Example: Complete Implementation of OrderService
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final UserRepository userRepository;
public OrderService(OrderRepository orderRepository,
ProductRepository productRepository,
UserRepository userRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.userRepository = userRepository;
}
@Transactional(rollbackFor = Exception.class)
public OrderResponse createOrder(CreateOrderRequest request, String username) {
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);
return OrderResponse.from(saved);
}
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(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");
}
@Transactional(readOnly = true)
public OrderResponse getOrder(Long orderId) {
Order order = orderRepository.findByIdWithItems(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", orderId));
return OrderResponse.from(order);
}
}
Output:
// Execution Successful
6. Integrating DTOs with Validation
(1) ▶ Example: Request/Response DTO
// Request DTO with validation
public record CreateOrderRequest(
@NotNull(message = "Product ID is required")
Long productId,
@Min(value = 1, message = "Quantity must be at least 1")
@Max(value = 100, message = "Quantity cannot exceed 100")
Integer quantity
) {}
// Response DTO
public record OrderResponse(
Long id,
String username,
String status,
BigDecimal totalAmount,
List<OrderItemResponse> items,
Instant createdAt
) {
public static OrderResponse from(Order order) {
List<OrderItemResponse> items = order.getItems().stream()
.map(item -> new OrderItemResponse(
item.getId(),
item.getProduct().getName(),
item.getQuantity(),
item.getUnitPrice(),
item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity()))
))
.toList();
return new OrderResponse(
order.getId(),
order.getUser().getUsername(),
order.getStatus(),
order.getTotalAmount(),
items,
order.getCreatedAt()
);
}
}
public record OrderItemResponse(
Long id,
String productName,
Integer quantity,
BigDecimal unitPrice,
BigDecimal subtotal
) {}
Output:
// Execution Successful
7. Security Integration
(1) ▶ Example: Custom UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found: " + username));
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
// OrderSecurity helper for SpEL expressions
@Component("orderSecurity")
public class OrderSecurity {
private final OrderRepository orderRepository;
public OrderSecurity(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public boolean isOwner(Long orderId, Authentication auth) {
String username = auth.getName();
return orderRepository.findById(orderId)
.map(order -> order.getUser().getUsername().equals(username))
.orElse(false);
}
}
Output:
// Execution Successful
8. Comprehensive Example: End-to-End Business Process Testing
# 1. Seed product data
curl -u alice:admin123 -X POST http://localhost:8080/api/v1/products \
-H "Content-Type: application/json" \
-d '{"name":"Laptop Pro","price":1299.99,"stock":50,"sku":"LAP-0001"}'
# 2. Create order as customer
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
-H "Content-Type: application/json" \
-d '{"productId":1,"quantity":3}'
# 3. View own order (customer bob)
curl -u bob:pass123 http://localhost:8080/api/v1/orders/1
# 4. Try invalid quantity -> 400
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
-H "Content-Type: application/json" \
-d '{"productId":1,"quantity":0}'
# 5. Try ordering more than stock -> 409
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
-H "Content-Type: application/json" \
-d '{"productId":1,"quantity":999}'
# 6. Cancel order as admin
curl -u alice:admin123 -X DELETE http://localhost:8080/api/v1/orders/1
| Test Scenario | Expected Result | Verification Points |
|---|---|---|
| Create Order | 201 Created | Transaction Guarantee: Orders and Inventory Are Updated Simultaneously |
| View My Orders | 200 OK | Access Control: Users can only view their own orders |
| Invalid Count | 400 VALIDATION_ERROR | Bean Validation Enabled |
| Insufficient Stock | 409 INSUFFICIENT_STOCK | Business Exception → Global Exception Handling |
| Cancel Order | 204 No Content | Transaction: Inventory Restoration |
| Unauthenticated Access | 401 Unauthorized | Spring Security Enabled |
❓ FAQ
logging.level.org.springframework.security=DEBUG to view the access decision logs. You can also add logs to the Bean methods referenced in the SpEL expression.@WithMockUser(roles="ADMIN") Simulate the ADMIN role.📖 Summary
- Integrate JPA, Services, Validation, Exceptions, and Security to build a complete business workflow
- Ordering Process: Validate input → Check inventory → Deduct inventory → Create order → Commit transaction
- Clear responsibilities for each module: Controller (validation/authorization) → Service (business logic/transactions) → Repository (data)
- Customize UserDetailsService to replace in-memory users; OrderSecurity helper method-level permission checks
- End-to-end testing covers normal flow + failure verification + business exceptions + permission denials
📝 Exercises
-
Basic Exercise (Difficulty: ⭐): Complete all code integration for this lesson and ensure that the end-to-end processes for placing, querying, and canceling orders all pass the curl tests.
-
Advanced Exercise (Difficulty ⭐⭐): Add a product management module (ProductController + ProductService) that allows users with the ADMIN role to create, update, and delete products, while regular users can only query them. Include comprehensive validation and exception handling.
-
Challenge (Difficulty: ⭐⭐⭐): Use
@SpringBootTest+@WithMockUserto write integration tests that cover scenarios such as successful order placement, insufficient inventory, and permission denial, achieving a test coverage rate of at least 80% for the core process.



