During one of the technical interviews I faced, I was asked to design an e-commerce system that allows users to purchase internet credits from third-party providers.
Confidently, I proposed a straightforward solution: display available packages, let users select one, process payments via an external gateway, and interact with the provider to deliver the credits. However, when asked about failure scenarios—like the provider running out of stock after a user completes payment—I realized my design lacked the resilience to handle such issues effectively.
A few weeks ago, I conducted research into flash sale systems and inventory reservation patterns, particularly focusing on inventory reservation strategies. Flash sales often deal with high demand and limited stock, requiring sophisticated mechanisms to maintain system stability and manage customer expectations. One concept I discovered was temporary inventory reservations, which help prevent overselling during peak times.
This research reminded me of my interview experience. I recognized that applying these inventory reservation strategies could have addressed the shortcomings in my initial design. By incorporating temporary holds on inventory during the checkout process, the system could effectively handle scenarios where the provider's stock is depleted.
In this research documentation, I aim to share the insights gained from my research and propose a refined approach to designing an internet credit purchase system. By integrating inventory reservation strategies, we can build a platform that is both robust and user-friendly, capable of handling various failure scenarios while providing a seamless experience.
When designing an internet credit purchasing system, there are a few key factors to consider to ensure a seamless, secure, and enjoyable user experience. Let’s break them down:
By taking these considerations into account, we can design an internet credit purchasing system that is efficient, secure, and user-friendly, leading to higher user satisfaction and trust.
Building on the foundational considerations outlined above, the next step is translating these principles into a robust and effective system design. By carefully mapping out the interactions between various components, we can ensure that the system not only meets functional requirements but also provides a seamless user experience while maintaining reliability and scalability.
In this section, we will delve into the system’s architecture and flow, showcasing how the core functionalities—like quota management, payment processing, and service activation—are implemented cohesively. The aim is to highlight how each design choice contributes to addressing potential challenges and delivering a dependable e-commerce credit purchasing platform.
Let’s start with an overview of the system’s flow, visualized through a flowchart, to illustrate how users interact with the system from start to finish.
The system's flow is divided into six phases for clarity:
This flow ensures a smooth, reliable experience for users, while also managing resources and potential errors effectively.
The sequence diagram below helps to illustrate the interaction between different roles and components.
The system's flow is divided into six phases for clarity:
Now that we’ve outlined the system’s flow and interactions, it’s time to dive into how it all comes together in code. This section breaks down the implementation step by step, showing how the design is translated into working parts that handle everything from managing orders to interacting with providers and payment systems.
// Domain Models @Getter @Setter @Entity public class Package { @Id private String id; private String name; private BigDecimal price; private BigDecimal providerCost; private String description; private boolean active; } @Getter @Setter @Entity public class Order { @Id private String id; private String customerId; private String packageId; private String reservationId; private String paymentId; private String escrowId; private OrderStatus status; private BigDecimal amount; private BigDecimal providerCost; private LocalDateTime createdAt; private LocalDateTime updatedAt; } @Getter @Setter @Entity public class QuotaReservation { @Id private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } // Enums public enum OrderStatus { CREATED, RESERVED, PAYMENT_PENDING, PAYMENT_COMPLETED, IN_ESCROW, ACTIVATING, ACTIVATION_FAILED, COMPLETED, REFUNDED } public enum ReservationStatus { ACTIVE, EXPIRED, USED, CANCELLED }
Here’s what these classes do:
Package: This is where we define the internet credit packages that users can purchase. It keeps track of details like the package ID, name, price, provider cost, description, and whether the package is active or not.
Order: Think of this as a record of user purchases. It includes information such as the order ID, customer ID, the selected package ID, and related details like the reservation ID, payment ID, escrow ID, order status, payment amount, provider cost, and timestamps.
QuotaReservation: This handles temporary reservations for package quotas. It logs the reservation ID, the package it’s tied to, when it expires, and its current status (like active or expired).
OrderStatus Enum: This enum maps out all the possible stages an order can go through, from CREATED and RESERVED to PAYMENT_PENDING, COMPLETED, or even REFUNDED.
ReservationStatus Enum: Similarly, this enum tracks the state of a quota reservation, whether it’s ACTIVE, EXPIRED, USED, or CANCELLED.
Together, these classes and enums build the backbone for managing packages, orders, and quota reservations in the system. It’s a simple yet structured approach to handle e-commerce functionality effectively.
// Request/Response DTOs @Getter @Setter public class OrderRequest { private String customerId; private String packageId; private BigDecimal amount; } @Getter @Setter public class PaymentCallback { private String orderId; private String paymentId; private String status; private BigDecimal amount; private LocalDateTime timestamp; } @Getter @Setter public class QuotaResponse { private String packageId; private boolean available; private Integer remainingQuota; private LocalDateTime timestamp; } @Getter @Setter public class ReservationResponse { private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } @Getter @Setter public class ActivationResponse { private String orderId; private boolean success; private String activationId; private String errorCode; private String errorMessage; } @Getter @Setter public class VerificationResponse { private String orderId; private String activationId; private boolean success; private String status; private LocalDateTime activatedAt; } @Getter @Setter public class PaymentRequest { private String orderId; private BigDecimal amount; private String currency; private String customerId; private String returnUrl; private String callbackUrl; } @Getter @Setter public class PaymentSession { private String sessionId; private String paymentUrl; private LocalDateTime expiresAt; private String status; } @Getter @Setter public class EscrowResponse { private String id; private String paymentId; private BigDecimal amount; private String status; private LocalDateTime createdAt; }
Let’s break it down:
OrderRequest: This is all about the data needed to create a new order. It includes the customer ID, the package they want to buy, and the total amount they’ll pay.
PaymentCallback: Think of this as a notification from the payment gateway. After a payment attempt, it provides details like the order ID, payment ID, status (success or failure), the amount paid, and when the payment happened.
QuotaResponse: This one’s about checking availability. It tells us if a package is available, how much quota is left, and when the information was last updated.
ReservationResponse: Once a package is reserved, this gives you all the details: the reservation ID, the associated package, when the reservation expires, and its current status (like active or expired).
ActivationResponse: This tells us how the service activation went. If it succeeded or failed, it gives us an activation ID and error details if something went wrong.
VerificationResponse: After activation, we verify if everything went smoothly. This includes the order ID, activation ID, success status, and the time it was activated.
PaymentRequest: Before starting the payment process, this DTO collects the necessary details like the order ID, the amount to be paid, the currency, customer ID, and callback URLs.
PaymentSession: This is what gets created when the payment process kicks off. It includes the session ID, the payment URL (where the user goes to pay), when it expires, and the session status.
EscrowResponse: If the funds are held in escrow, this tells us all about it—like the escrow ID, payment ID, the amount held, status, and when it was created.
All of these classes define the building blocks for communication between different parts of the system—whether it’s requests going out or responses coming back. They ensure everyone (and everything) is on the same page.
// Domain Models @Getter @Setter @Entity public class Package { @Id private String id; private String name; private BigDecimal price; private BigDecimal providerCost; private String description; private boolean active; } @Getter @Setter @Entity public class Order { @Id private String id; private String customerId; private String packageId; private String reservationId; private String paymentId; private String escrowId; private OrderStatus status; private BigDecimal amount; private BigDecimal providerCost; private LocalDateTime createdAt; private LocalDateTime updatedAt; } @Getter @Setter @Entity public class QuotaReservation { @Id private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } // Enums public enum OrderStatus { CREATED, RESERVED, PAYMENT_PENDING, PAYMENT_COMPLETED, IN_ESCROW, ACTIVATING, ACTIVATION_FAILED, COMPLETED, REFUNDED } public enum ReservationStatus { ACTIVE, EXPIRED, USED, CANCELLED }
This service takes care of a local cache that stores package data. The goal is to make the system faster and reduce unnecessary calls to the provider's API.
This service handles communication with the provider's API. It manages tasks like checking quotas, reserving packages, activating services, and verifying those activations.
The service uses RetryTemplate to automatically retry requests to the provider’s API when there are temporary issues. This ensures the system stays reliable and resilient even during minor hiccups.
By combining these features, this code ensures the system efficiently manages package data while maintaining smooth and dependable communication with the provider's API.
// Domain Models @Getter @Setter @Entity public class Package { @Id private String id; private String name; private BigDecimal price; private BigDecimal providerCost; private String description; private boolean active; } @Getter @Setter @Entity public class Order { @Id private String id; private String customerId; private String packageId; private String reservationId; private String paymentId; private String escrowId; private OrderStatus status; private BigDecimal amount; private BigDecimal providerCost; private LocalDateTime createdAt; private LocalDateTime updatedAt; } @Getter @Setter @Entity public class QuotaReservation { @Id private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } // Enums public enum OrderStatus { CREATED, RESERVED, PAYMENT_PENDING, PAYMENT_COMPLETED, IN_ESCROW, ACTIVATING, ACTIVATION_FAILED, COMPLETED, REFUNDED } public enum ReservationStatus { ACTIVE, EXPIRED, USED, CANCELLED }
This class plays a key role in managing how the system interacts with the payment gateway to handle financial transactions smoothly and securely.
This class is a crucial piece of the puzzle when it comes to managing secure and efficient financial transactions in the system.
// Domain Models @Getter @Setter @Entity public class Package { @Id private String id; private String name; private BigDecimal price; private BigDecimal providerCost; private String description; private boolean active; } @Getter @Setter @Entity public class Order { @Id private String id; private String customerId; private String packageId; private String reservationId; private String paymentId; private String escrowId; private OrderStatus status; private BigDecimal amount; private BigDecimal providerCost; private LocalDateTime createdAt; private LocalDateTime updatedAt; } @Getter @Setter @Entity public class QuotaReservation { @Id private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } // Enums public enum OrderStatus { CREATED, RESERVED, PAYMENT_PENDING, PAYMENT_COMPLETED, IN_ESCROW, ACTIVATING, ACTIVATION_FAILED, COMPLETED, REFUNDED } public enum ReservationStatus { ACTIVE, EXPIRED, USED, CANCELLED }
This service handles all the notifications sent to users about their order status. Here's how it works:
This service ensures that users are always in the loop about their orders, whether it's through email, SMS, or real-time updates.
// Domain Models @Getter @Setter @Entity public class Package { @Id private String id; private String name; private BigDecimal price; private BigDecimal providerCost; private String description; private boolean active; } @Getter @Setter @Entity public class Order { @Id private String id; private String customerId; private String packageId; private String reservationId; private String paymentId; private String escrowId; private OrderStatus status; private BigDecimal amount; private BigDecimal providerCost; private LocalDateTime createdAt; private LocalDateTime updatedAt; } @Getter @Setter @Entity public class QuotaReservation { @Id private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } // Enums public enum OrderStatus { CREATED, RESERVED, PAYMENT_PENDING, PAYMENT_COMPLETED, IN_ESCROW, ACTIVATING, ACTIVATION_FAILED, COMPLETED, REFUNDED } public enum ReservationStatus { ACTIVE, EXPIRED, USED, CANCELLED }
This is where all the WebSocket magic happens! It manages real-time updates between the server and clients.
This setup ensures smooth and instant communication between the backend and the front-end, so users always have up-to-date information on quota availability and order statuses.
// Domain Models @Getter @Setter @Entity public class Package { @Id private String id; private String name; private BigDecimal price; private BigDecimal providerCost; private String description; private boolean active; } @Getter @Setter @Entity public class Order { @Id private String id; private String customerId; private String packageId; private String reservationId; private String paymentId; private String escrowId; private OrderStatus status; private BigDecimal amount; private BigDecimal providerCost; private LocalDateTime createdAt; private LocalDateTime updatedAt; } @Getter @Setter @Entity public class QuotaReservation { @Id private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } // Enums public enum OrderStatus { CREATED, RESERVED, PAYMENT_PENDING, PAYMENT_COMPLETED, IN_ESCROW, ACTIVATING, ACTIVATION_FAILED, COMPLETED, REFUNDED } public enum ReservationStatus { ACTIVE, EXPIRED, USED, CANCELLED }
Here’s a breakdown of these custom exception classes and how they’re used to handle specific error scenarios in the system:
QuotaNotAvailableException:
OrderNotFoundException:
PaymentVerificationException:
By using these exceptions, the system handles errors in a clean and predictable way. They not only make debugging more efficient for developers but also ensure users receive clear and actionable feedback when something goes wrong.
// Request/Response DTOs @Getter @Setter public class OrderRequest { private String customerId; private String packageId; private BigDecimal amount; } @Getter @Setter public class PaymentCallback { private String orderId; private String paymentId; private String status; private BigDecimal amount; private LocalDateTime timestamp; } @Getter @Setter public class QuotaResponse { private String packageId; private boolean available; private Integer remainingQuota; private LocalDateTime timestamp; } @Getter @Setter public class ReservationResponse { private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } @Getter @Setter public class ActivationResponse { private String orderId; private boolean success; private String activationId; private String errorCode; private String errorMessage; } @Getter @Setter public class VerificationResponse { private String orderId; private String activationId; private boolean success; private String status; private LocalDateTime activatedAt; } @Getter @Setter public class PaymentRequest { private String orderId; private BigDecimal amount; private String currency; private String customerId; private String returnUrl; private String callbackUrl; } @Getter @Setter public class PaymentSession { private String sessionId; private String paymentUrl; private LocalDateTime expiresAt; private String status; } @Getter @Setter public class EscrowResponse { private String id; private String paymentId; private BigDecimal amount; private String status; private LocalDateTime createdAt; }
The OrderService class handles the heavy lifting when it comes to managing orders. Let’s break down how it works:
createOrder(OrderRequest request):
processPayment(String orderId, PaymentCallback callback):
verifyActivation(Order order):
completeOrder(Order order):
handleActivationFailure(Order order):
getOrder(String orderId):
This service is the backbone of the order management process, tying everything together for a seamless user experience.
// Domain Models @Getter @Setter @Entity public class Package { @Id private String id; private String name; private BigDecimal price; private BigDecimal providerCost; private String description; private boolean active; } @Getter @Setter @Entity public class Order { @Id private String id; private String customerId; private String packageId; private String reservationId; private String paymentId; private String escrowId; private OrderStatus status; private BigDecimal amount; private BigDecimal providerCost; private LocalDateTime createdAt; private LocalDateTime updatedAt; } @Getter @Setter @Entity public class QuotaReservation { @Id private String id; private String packageId; private LocalDateTime expiresAt; private ReservationStatus status; } // Enums public enum OrderStatus { CREATED, RESERVED, PAYMENT_PENDING, PAYMENT_COMPLETED, IN_ESCROW, ACTIVATING, ACTIVATION_FAILED, COMPLETED, REFUNDED } public enum ReservationStatus { ACTIVE, EXPIRED, USED, CANCELLED }
The OrderController class takes care of the REST API endpoints that manage orders in the system. Think is the bridge between the client making requests and the backend services doing the heavy lifting.
POST /api/orders (createOrder):
POST /api/orders/callback (handlePaymentCallback):
GET /api/orders/{orderId} (getOrder):
This controller ensures that the client and backend communicate seamlessly, making order management as smooth as possible.
This research documentation lays out the foundation for designing an e-commerce credit sales system, tackling important challenges like quota management, payment processing, and service activation. While this design covers the basics, there’s always room to make things better!
Here are a few ideas to improve this design:
Thanks so much for reading! I hope this documentation has been useful and provides clarity for anyone exploring similar challenges. Of course, this design isn’t perfect—there’s always room for improvement. If you have any thoughts or suggestions, I’d love to hear them.
resources:
The above is the detailed content of Designing an Internet Credit Purchase System. For more information, please follow other related articles on the PHP Chinese website!