404 Not Found

404 Not Found


nginx

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


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:

100%
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

JAVA
@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:

TEXT
// Execution Successful

(2) ▶ Example: Registration and Login

JAVA
@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:

TEXT
// Execution Successful

4. Product Module

(1) ▶ Example: ProductService with Caching

JAVA
@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:

TEXT
// Execution Successful

5. Order Module

(1) ▶ Example: OrderService Order Transaction

JAVA
@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:

TEXT
// Execution Successful

6. Payment Module

(1) ▶ Example: PaymentService with Idempotency

JAVA
@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:

TEXT
// Execution Successful

7. Global Properties

(1) ▶ Example: SpringDoc API Documentation Configuration

JAVA
@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:

TEXT
// 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

TEXT
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

Q Should I write the Entity first, or the Controller?
A We recommend a bottom-up approach: Entity → Repository → Service → Controller. First, define the data model; then implement data access; next, write the business logic; and finally, implement the API layer. This way, each layer can be tested independently.
Q How do we ensure the idempotency of payment callbacks?
A 1) Redis SETNX ensures that a payment is initiated only once per order; 2) Database unique constraint (transaction_id); 3) Payment state machine (PENDING → COMPLETED is irreversible); 4) Log all callback events for reconciliation purposes.
Q How does the two-tier cache ensure consistency?
A During an update: 1) Update the database first; 2) Delete the Redis cache; 3) Notify all instances via Redis Pub/Sub to clear their local Caffeine caches. For reads: First check Caffeine → then check Redis → finally check the database.
Q How are the API documentation and the code kept in sync?
A SpringDoc automatically scans Controller annotations to generate OpenAPI documentation; when the code changes, the documentation updates automatically. However, you need to add the @Schema annotation to DTOs to provide additional explanations.
Q What are the naming conventions for Flyway migration scripts?
A 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.
Q How do you organize collaborative development among multiple people?
A 1) Create Git branches by module (feature/user, feature/order); 2) Integrate changes into the develop branch daily; 3) Merge after code review; 4) Run tests automatically via CI. The key is to ensure that each module is self-contained and testable.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Implement complete code for user registration/login and product CRUD operations, ensuring that JWT authentication and caching are enabled.

  2. 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.

  3. 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏