Designing an Internet Credit Purchase System
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. 1. Key Design Considerations 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: 1.1 Quota Management Real-time Quota Verification: The system should instantly check if the internet credit packages are in stock, so users don’t accidentally select unavailable options. Temporary Quota Reservation: Add a mechanism to hold a selected package for a short period, giving users enough time to complete their purchase without the risk of losing the item. Handling Quota Conflicts: Develop strategies to manage situations where multiple users try to buy the same package at the same time, ensuring fair allocation. Cache Management for Package Information: Keep cache data accurate and up-to-date so users always see the right details and availability. 1.2 Payment Processing Secure Payment Handling: Implement strong security measures to protect users’ payment details during transactions. Escrow System for Payment Protection: Use an escrow service to hold funds until the credits are delivered, keeping both buyers and providers safe. Payment Gateway Integration: Make sure the system connects smoothly with reliable payment gateways to ensure hassle-free transactions. Refund Mechanisms: Create clear and user-friendly processes for issuing refunds in case of failed payments or cancellations. 1.3 Provider Integration System Availability: Partner with providers who have reliable systems to ensure purchases are processed without disruptions. API Reliability: Work with providers offering stable, well-documented APIs for seamless integration. Service Activation Verification: Include checks to confirm that purchased credits are activated properly and promptly. Error Handling and Retries: Implement protocols to catch and resolve errors quickly, with retry mechanisms for any failed processes. 1.4 Transaction Safety Money Flow Control: Ensure funds are only released after transactions are completed successfully. Transaction Consistency: Keep accurate and consistent records of all transactions to prevent errors. Rollback Mechanisms: Have a plan to revert transactions if something goes wrong, protecting both users and the system. Audit Trail: Maintain detailed logs to help monitor and troubleshoot any issues effectively. 1.5 User Experience Clear Error Messages: Provide users with understandable and informative error messages to guide them through any issues encountered. Transaction Status Visibility: Allow users to easily track the status of their purchases in real-time, enhancing transparency. Quick Package Loading: Optimize the system to load available packages swiftly, reducing waiting times for users. Real-time Updates: Keep users informed of any changes or updates to their transactions or available packages promptly. 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. 2. System Design and Flow Building on the foundational considerations outlined above, the next step is translating these principles into a robust and effective s
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.
1. Key Design Considerations
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:
1.1 Quota Management
- Real-time Quota Verification: The system should instantly check if the internet credit packages are in stock, so users don’t accidentally select unavailable options.
- Temporary Quota Reservation: Add a mechanism to hold a selected package for a short period, giving users enough time to complete their purchase without the risk of losing the item.
- Handling Quota Conflicts: Develop strategies to manage situations where multiple users try to buy the same package at the same time, ensuring fair allocation.
- Cache Management for Package Information: Keep cache data accurate and up-to-date so users always see the right details and availability.
1.2 Payment Processing
- Secure Payment Handling: Implement strong security measures to protect users’ payment details during transactions.
- Escrow System for Payment Protection: Use an escrow service to hold funds until the credits are delivered, keeping both buyers and providers safe.
- Payment Gateway Integration: Make sure the system connects smoothly with reliable payment gateways to ensure hassle-free transactions.
- Refund Mechanisms: Create clear and user-friendly processes for issuing refunds in case of failed payments or cancellations.
1.3 Provider Integration
- System Availability: Partner with providers who have reliable systems to ensure purchases are processed without disruptions.
- API Reliability: Work with providers offering stable, well-documented APIs for seamless integration.
- Service Activation Verification: Include checks to confirm that purchased credits are activated properly and promptly.
- Error Handling and Retries: Implement protocols to catch and resolve errors quickly, with retry mechanisms for any failed processes.
1.4 Transaction Safety
- Money Flow Control: Ensure funds are only released after transactions are completed successfully.
- Transaction Consistency: Keep accurate and consistent records of all transactions to prevent errors.
- Rollback Mechanisms: Have a plan to revert transactions if something goes wrong, protecting both users and the system.
- Audit Trail: Maintain detailed logs to help monitor and troubleshoot any issues effectively.
1.5 User Experience
- Clear Error Messages: Provide users with understandable and informative error messages to guide them through any issues encountered.
- Transaction Status Visibility: Allow users to easily track the status of their purchases in real-time, enhancing transparency.
- Quick Package Loading: Optimize the system to load available packages swiftly, reducing waiting times for users.
- Real-time Updates: Keep users informed of any changes or updates to their transactions or available packages promptly.
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.
2. System Design and Flow
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.
2.1 Flowchart Diagram
The system's flow is divided into six phases for clarity:
Package Selection Phase
- First, the user visits the package selection page, where the app fetches package data from a cache. This data includes available packages and their cached quota information, which is then displayed to the user.
- The user picks a package and clicks "Buy."
- If the quota for that package isn’t available, the app shows a "Not Available" message and takes the user back to the selection page. Otherwise, the system temporarily reserves the quota for the user.
Purchase Initiation
- Next, the system attempts to reserve the quota for the chosen package.
- If the reservation fails, the user sees an error message and is redirected back to the selection page.
- If the reservation is successful, the user moves forward to the payment page.
Payment Phase
- At this stage, the user starts the payment process and gets redirected to a third-party payment gateway.
- The app waits for a response (callback) from the payment gateway to confirm the payment status.
Payment Processing
- The app checks the payment gateway's callback to validate the payment:
- For invalid callbacks, the system logs the issue and halts further steps.
- For valid callbacks:
- If the payment fails: The system releases the reserved quota and informs the user about the issue.
- If the payment succeeds: The system verifies the payment, holds the funds in escrow, and creates a new order.
Service Activation
- Once the payment is successful, the system asks the provider to activate the service.
- If the activation fails: The escrow funds are refunded to the customer, and they’re notified about the failure.
- If the activation succeeds: The system verifies the activation.
- If the verification fails, the customer gets a refund.
- If the verification succeeds, the escrow funds are released to the provider, and the customer receives a notification.
Background Processes
- Periodic Cache Updates: Package data cache is updated regularly.
- Real-time Quota Updates: Quota changes are communicated via WebSocket connections.
This flow ensures a smooth, reliable experience for users, while also managing resources and potential errors effectively.
2.2 Sequence Diagram
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:
Package Selection Phase
- The customer starts by visiting the package selection page.
- The frontend retrieves package data from the cache and displays all the available packages, along with their cached quota information, to the customer.
Purchase Initiation
- Once the customer selects a package and clicks "Buy," the frontend sends a purchase request to the backend.
- The backend checks with the provider to see if the selected package’s quota is still available in real time.
- If the quota is available, the backend reserves it temporarily with the provider for 15 minutes.
- The backend then sends a reservation confirmation to the frontend, and the customer is redirected to the payment page.
Payment Phase
- The customer proceeds to the payment page and submits their payment details.
- The frontend sends this information to the backend, which initializes a payment session with the payment gateway.
- Once the payment session is ready, the backend shares the session details with the frontend.
- The frontend redirects the customer to the payment gateway to complete the payment.
Payment Processing
- At the payment gateway, the customer enters their payment information and completes the payment process.
- The payment gateway notifies the backend of the payment status through a callback:
- If the payment is successful:
- The backend verifies the payment status with the payment gateway.
- The payment is held in escrow, and the backend confirms the escrow hold.
- If the payment fails:
- The backend releases the reserved quota with the provider and updates the payment status to reflect the failure.
- If no callback is received within the expected time:
- The backend periodically polls the payment gateway to check the payment status. If the payment fails or times out, the backend releases the reserved quota.
- If the payment is successful:
Service Activation
- If the payment is successful, the backend requests the provider to activate the service.
- The provider responds with the activation status, and the backend verifies the activation:
- If the activation is successful:
- The backend releases the payment from escrow to the provider.
- A success notification is sent to the customer, letting them know the service is ready.
- If the activation fails:
- The backend refunds the escrowed funds to the customer.
- A failure notification is sent to inform the customer about the issue.
- If the activation is successful:
Background Processes
- The provider sends real-time updates about package quotas to the backend via WebSocket connections.
- These updates ensure the cache is always up-to-date, so customers see the most accurate package availability when browsing.
3. Technical Implementation
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
andRESERVED
toPAYMENT_PENDING
,COMPLETED
, or evenREFUNDED
.ReservationStatus Enum: Similarly, this enum tracks the state of a quota reservation, whether it’s
ACTIVE
,EXPIRED
,USED
, orCANCELLED
.
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.
// Cache Service
@Service
@Slf4j
public class PackageCacheService {
private final Cache packageCache;
private final ProviderClient providerClient;
@Scheduled(fixedRate = 300000) // 5 minutes
public void updateCache() {
try {
List packages = providerClient.getAllPackages();
packages.forEach(pkg ->
packageCache.put(pkg.getId(), pkg));
} catch (Exception e) {
log.error("Failed to update package cache", e);
}
}
public Package getPackage(String id) {
return packageCache.get(id);
}
public void updatePackageQuota(QuotaUpdate update) {
Package pkg = packageCache.get(update.getPackageId());
if (pkg != null) {
// Update quota information
packageCache.put(update.getPackageId(), pkg);
}
}
}
// Provider Integration
@Service
public class ProviderClient {
private final WebClient webClient;
private final RetryTemplate retryTemplate;
public QuotaResponse checkQuota(String packageId) {
return retryTemplate.execute(context ->
webClient.get()
.uri("/packages/{id}/quota", packageId)
.retrieve()
.bodyToMono(QuotaResponse.class)
.block()
);
}
public ReservationResponse reserveQuota(String packageId) {
return webClient.post()
.uri("/packages/{id}/reserve", packageId)
.retrieve()
.bodyToMono(ReservationResponse.class)
.block();
}
public ActivationResponse activateService(String orderId) {
return webClient.post()
.uri("/orders/{id}/activate", orderId)
.retrieve()
.bodyToMono(ActivationResponse.class)
.block();
}
public VerificationResponse verifyActivation(String orderId) {
return webClient.get()
.uri("/orders/{id}/verify", orderId)
.retrieve()
.bodyToMono(VerificationResponse.class)
.block();
}
public List getAllPackages() {
return webClient.get()
.uri("/packages")
.retrieve()
.bodyToFlux(Package.class)
.collectList()
.block();
}
}
Cache Service
1. Purpose:
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.
2. Key Features:
-
updateCache()
: This method refreshes the local cache every 5 minutes by fetching all package data from the provider. It ensures the cache stays up to date. -
getPackage()
: This method retrieves package info from the cache using its ID. -
updatePackageQuota()
: When quota details change, this method updates the cache with the new information for a specific package.
Provider Integration
1. Purpose:
This service handles communication with the provider's API. It manages tasks like checking quotas, reserving packages, activating services, and verifying those activations.
2. Key Features:
-
checkQuota()
: This method checks if a package has enough quota available by calling the provider's API. -
reserveQuota()
: It reserves a package's quota for a customer by sending a request to the provider. -
activateService()
: When it's time to activate a service for an order, this method handles the request to the provider. -
verifyActivation()
: After activation, this method confirms whether everything was successful. -
getAllPackages()
: This method retrieves all available packages from the provider, which is useful for updating the cache or displaying package options to users.
3. Retry Mechanism:
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.
// Payment Gateway Integration
@Service
public class PaymentGatewayClient {
private final WebClient webClient;
public PaymentSession initializePayment(PaymentRequest request) {
return webClient.post()
.uri("/payments/initialize")
.body(request)
.retrieve()
.bodyToMono(PaymentSession.class)
.block();
}
public EscrowResponse holdInEscrow(String paymentId) {
return webClient.post()
.uri("/payments/{id}/escrow", paymentId)
.retrieve()
.bodyToMono(EscrowResponse.class)
.block();
}
public void releaseToProvider(String escrowId) {
webClient.post()
.uri("/escrow/{id}/release", escrowId)
.retrieve()
.bodyToMono(Void.class)
.block();
}
public void refundToCustomer(String escrowId) {
webClient.post()
.uri("/escrow/{id}/refund", escrowId)
.retrieve()
.bodyToMono(Void.class)
.block();
}
}
Payment Gateway Integration
This class plays a key role in managing how the system interacts with the payment gateway to handle financial transactions smoothly and securely.
1. initializePayment(PaymentRequest request)
:
- Think of this as starting the payment process. It sends a request to the payment gateway with all the payment details.
- It returns a
PaymentSession
object, which includes information like the payment URL and session status.
2. holdInEscrow(String paymentId)
:
- This method secures the payment in an escrow account using the given payment ID.
- It provides an
EscrowResponse
object that contains all the details about the escrowed funds.
3. releaseToProvider(String escrowId)
:
- After the service is successfully activated, this method releases the funds from escrow to the service provider.
- The escrow ID is used to identify and release the correct funds.
4. refundToCustomer(String escrowId)
:
- If something goes wrong—like the service activation fails, this method refunds the escrowed funds back to the customer.
- It uses the escrow ID to process the refund.
Key Features:
- The class uses
WebClient
to send HTTP requests to the payment gateway's REST API, ensuring seamless integration. - It handles critical operations like starting payments, managing escrow, and processing refunds.
- All methods use synchronous calls (via
.block()
) to make sure operations are completed before moving on, ensuring reliability.
This class is a crucial piece of the puzzle when it comes to managing secure and efficient financial transactions in the system.
// Notification DTOs
@Getter @Setter
public class EmailNotification {
private String to;
private String subject;
private String templateId;
private Map templateData;
}
@Getter @Setter
public class SmsNotification {
private String phoneNumber;
private String templateId;
private Map templateData;
}
// Notification Service
@Service
@Slf4j
public class NotificationService {
private final EmailService emailService;
private final SmsService smsService;
private final QuotaWebSocketHandler webSocketHandler;
public void sendSuccessNotification(Order order) {
try {
EmailNotification email = buildSuccessEmail(order);
emailService.sendEmail(email);
SmsNotification sms = buildSuccessSms(order);
smsService.sendSms(sms);
webSocketHandler.sendOrderUpdate(order);
} catch (Exception e) {
log.error("Failed to send success notification for order: " + order.getId(), e);
}
}
public void sendFailureNotification(Order order) {
try {
EmailNotification email = buildFailureEmail(order);
emailService.sendEmail(email);
SmsNotification sms = buildFailureSms(order);
smsService.sendSms(sms);
webSocketHandler.sendOrderUpdate(order);
} catch (Exception e) {
log.error("Failed to send failure notification for order: " + order.getId(), e);
}
}
private EmailNotification buildSuccessEmail(Order order) {
EmailNotification notification = new EmailNotification();
notification.setSubject("Order Completed Successfully");
notification.setTemplateId("ORDER_SUCCESS");
notification.setTemplateData(Map.of(
"orderId", order.getId(),
"packageId", order.getPackageId(),
"amount", order.getAmount()
));
return notification;
}
private SmsNotification buildSuccessSms(Order order) {
SmsNotification notification = new SmsNotification();
notification.setTemplateId("ORDER_SUCCESS_SMS");
notification.setTemplateData(Map.of(
"orderId", order.getId()
));
return notification;
}
private EmailNotification buildFailureEmail(Order order) {
EmailNotification notification = new EmailNotification();
notification.setSubject("Order Processing Failed");
notification.setTemplateId("ORDER_FAILURE");
notification.setTemplateData(Map.of(
"orderId", order.getId(),
"packageId", order.getPackageId()
));
return notification;
}
private SmsNotification buildFailureSms(Order order) {
SmsNotification notification = new SmsNotification();
notification.setTemplateId("ORDER_FAILURE_SMS");
notification.setTemplateData(Map.of(
"orderId", order.getId()
));
return notification;
}
}
Notification DTOs
1. EmailNotification:
- Think of this as a blueprint for sending email notifications. It includes:
- The recipient's email (
to
). - The subject of the email.
- A template ID to determine the format.
- Dynamic data (
templateData
) to personalize the content.
- The recipient's email (
2. SmsNotification:
- Similar to the email notification but tailored for SMS. It includes:
- The recipient's phone number (
phoneNumber
). - A template ID for the message format.
- Dynamic data (
templateData
) for personalization.
- The recipient's phone number (
Notification Service
This service handles all the notifications sent to users about their order status. Here's how it works:
1. sendSuccessNotification(Order order)
:
- This method handles sending success notifications. It uses:
-
buildSuccessEmail
to create an email notification. -
buildSuccessSms
to create an SMS notification. - It also sends real-time updates through WebSocket using
QuotaWebSocketHandler
.
-
2. sendFailureNotification(Order order)
:
- This one takes care of failure notifications. It uses:
-
buildFailureEmail
for email messages. -
buildFailureSms
for SMS messages. - Like success notifications, it also sends WebSocket updates.
-
3. Helper Methods:
-
buildSuccessEmail
andbuildFailureEmail
: These methods create email notifications based on whether the order was successful or failed. They use templates and the order's details. -
buildSuccessSms
andbuildFailureSms
: These do the same but for SMS notifications.
Additional Features:
- WebSocket Updates: Keeps the front-end updated in real time using
QuotaWebSocketHandler
. - Error Logging: If something goes wrong, it logs the errors for debugging.
This service ensures that users are always in the loop about their orders, whether it's through email, SMS, or real-time updates.
// WebSocket Related Classes
@Getter @Setter
public class QuotaUpdate {
private String packageId;
private Integer availableQuota;
private LocalDateTime timestamp;
}
// WebSocket Configuration
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(quotaWebSocketHandler(), "/ws/quota")
.setAllowedOrigins("*");
}
@Bean
public WebSocketHandler quotaWebSocketHandler() {
return new QuotaWebSocketHandler();
}
}
@Component
@Slf4j
public class QuotaWebSocketHandler extends TextWebSocketHandler {
private final PackageCacheService cacheService;
private final ObjectMapper objectMapper;
private final Set sessions = new ConcurrentHashSet<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
}
@Override
protected void handleTextMessage(
WebSocketSession session,
TextMessage message) throws IOException {
QuotaUpdate update = objectMapper.readValue(message.getPayload(),
QuotaUpdate.class);
cacheService.updatePackageQuota(update);
}
public void sendOrderUpdate(Order order) {
TextMessage message = new TextMessage(objectMapper.writeValueAsString(order));
sessions.forEach(session -> {
try {
if (session.isOpen()) {
session.sendMessage(message);
}
} catch (IOException e) {
log.error("Failed to send order update to session", e);
}
});
}
}
QuotaUpdate Class
- Think of this class as a simple messenger for quota updates. It carries three key pieces of information:
-
packageId
: The ID of the package being updated. -
availableQuota
: How much quota is left for this package. -
timestamp
: When the update was made.
-
WebSocket Configuration
1. WebSocketConfig:
- This is the setup that makes WebSocket communication possible.
- It registers a handler (
quotaWebSocketHandler
) to listen for WebSocket connections at/ws/quota
. - It also allows connections from any origin by setting
allowedOrigins("*")
.
2. quotaWebSocketHandler()
:
- This defines the WebSocket handler bean that will manage incoming messages and connections.
QuotaWebSocketHandler
This is where all the WebSocket magic happens! It manages real-time updates between the server and clients.
1. Fields:
-
PackageCacheService
: Helps update the local cache whenever a quota update comes in. -
ObjectMapper
: Handles the conversion of JSON payloads to Java objects and vice versa. -
sessions
: Keeps track of all the active WebSocket sessions (clients currently connected).
2. Methods:
-
afterConnectionEstablished(WebSocketSession session)
:- Adds a new client session to the active list as soon as they connect.
-
afterConnectionClosed(WebSocketSession session, CloseStatus status)
:- Removes the client session when they disconnect.
-
handleTextMessage(WebSocketSession session, TextMessage message)
:- Processes incoming messages.
- Converts the received JSON into a
QuotaUpdate
object and updates the local cache.
3. sendOrderUpdate(Order order)
:
- Sends real-time updates about order changes to all connected clients.
- Converts the
Order
object to JSON and sends it as a message to active WebSocket sessions. - Makes sure only open connections receive updates.
Key Features of the Code:
- Real-time Updates:
- Keeps clients instantly informed about quota changes and order updates.
- Thread-Safe Management:
- Uses
ConcurrentHashSet
to handle connected clients, ensuring no conflicts when multiple clients are active.
- Uses
- Error Handling:
- Logs errors when there’s an issue sending messages, making it easier to troubleshoot.
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.
// Exception Classes
public class QuotaNotAvailableException extends RuntimeException {
public QuotaNotAvailableException() {
super("Package quota is not available");
}
}
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(String orderId) {
super("Order not found: " + orderId);
}
}
public class PaymentVerificationException extends RuntimeException {
public PaymentVerificationException(String message) {
super(message);
}
}
Here’s a breakdown of these custom exception classes and how they’re used to handle specific error scenarios in the system:
QuotaNotAvailableException
:
- This exception is triggered when a user tries to purchase a package, but the quota for that package is already gone.
- It comes with a straightforward default message: "Package quota is not available," so both developers and users get a clear understanding of the issue.
OrderNotFoundException
:
- This one kicks in when the system can’t find an order based on the provided
orderId
. - It includes a detailed error message like, "Order not found: [orderId]," making it easy to pinpoint exactly which order is missing.
PaymentVerificationException
:
- If there’s an issue verifying a payment—maybe the amounts don’t match, or the payment status is unclear—this exception gets thrown.
- It allows you to pass in a custom message, adding flexibility and context for diagnosing payment issues.
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.
// Order Service
@Service
@Transactional
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
private final ProviderClient providerClient;
private final PaymentGatewayClient paymentGatewayClient;
private final NotificationService notificationService;
private final PackageCacheService packageCacheService;
@Autowired
public OrderService(OrderRepository orderRepository,
ProviderClient providerClient,
PaymentGatewayClient paymentGatewayClient,
NotificationService notificationService,
PackageCacheService packageCacheService) {
this.orderRepository = orderRepository;
this.providerClient = providerClient;
this.paymentGatewayClient = paymentGatewayClient;
this.notificationService = notificationService;
this.packageCacheService = packageCacheService;
}
public Order createOrder(OrderRequest request) {
log.info("Creating new order for package: {}", request.getPackageId());
// Check quota
QuotaResponse quota = providerClient.checkQuota(request.getPackageId());
if (!quota.isAvailable()) {
log.warn("Quota not available for package: {}", request.getPackageId());
throw new QuotaNotAvailableException();
}
// Get package details
Package pkg = packageCacheService.getPackage(request.getPackageId());
// Reserve quota
ReservationResponse reservation = providerClient.reserveQuota(request.getPackageId());
// Create order
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setCustomerId(request.getCustomerId());
order.setPackageId(request.getPackageId());
order.setReservationId(reservation.getId());
order.setAmount(pkg.getPrice());
order.setProviderCost(pkg.getProviderCost());
order.setStatus(OrderStatus.RESERVED);
order.setCreatedAt(LocalDateTime.now());
order.setUpdatedAt(LocalDateTime.now());
Order savedOrder = orderRepository.save(order);
log.info("Order created successfully: {}", savedOrder.getId());
return savedOrder;
}
public Order processPayment(String orderId, PaymentCallback callback) {
log.info("Processing payment for order: {}", orderId);
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
try {
// Verify payment amount matches order amount
if (!order.getAmount().equals(callback.getAmount())) {
throw new PaymentVerificationException("Payment amount mismatch");
}
// Update order with payment details
order.setPaymentId(callback.getPaymentId());
order.setStatus(OrderStatus.PAYMENT_COMPLETED);
order.setUpdatedAt(LocalDateTime.now());
orderRepository.save(order);
// Hold payment in escrow
log.info("Holding payment in escrow for order: {}", orderId);
EscrowResponse escrow = paymentGatewayClient.holdInEscrow(callback.getPaymentId());
order.setEscrowId(escrow.getId());
order.setStatus(OrderStatus.IN_ESCROW);
orderRepository.save(order);
// Activate service
log.info("Initiating service activation for order: {}", orderId);
order.setStatus(OrderStatus.ACTIVATING);
orderRepository.save(order);
ActivationResponse activation = providerClient.activateService(orderId);
if (activation.isSuccess()) {
verifyActivation(order);
} else {
handleActivationFailure(order);
}
} catch (Exception e) {
log.error("Error processing payment for order: {}", orderId, e);
handleActivationFailure(order);
}
return order;
}
private void verifyActivation(Order order) {
log.info("Verifying activation for order: {}", order.getId());
int attempts = 0;
boolean activated = false;
while (attempts < 3 && !activated) {
try {
VerificationResponse verification =
providerClient.verifyActivation(order.getId());
if (verification.isSuccess()) {
activated = true;
completeOrder(order);
}
} catch (Exception e) {
log.error("Verification attempt {} failed for order: {}",
attempts + 1, order.getId(), e);
}
attempts++;
if (!activated && attempts < 3) {
try {
Thread.sleep(2000); // Wait before next attempt
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
if (!activated) {
handleActivationFailure(order);
}
}
private void completeOrder(Order order) {
log.info("Completing order: {}", order.getId());
try {
paymentGatewayClient.releaseToProvider(order.getEscrowId());
order.setStatus(OrderStatus.COMPLETED);
order.setUpdatedAt(LocalDateTime.now());
orderRepository.save(order);
notificationService.sendSuccessNotification(order);
log.info("Order completed successfully: {}", order.getId());
} catch (Exception e) {
log.error("Error completing order: {}", order.getId(), e);
handleActivationFailure(order);
}
}
private void handleActivationFailure(Order order) {
log.warn("Handling activation failure for order: {}", order.getId());
try {
paymentGatewayClient.refundToCustomer(order.getEscrowId());
order.setStatus(OrderStatus.REFUNDED);
order.setUpdatedAt(LocalDateTime.now());
orderRepository.save(order);
notificationService.sendFailureNotification(order);
log.info("Order refunded successfully: {}", order.getId());
} catch (Exception e) {
log.error("Error processing refund for order: {}", order.getId(), e);
}
}
public Order getOrder(String orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
}
The OrderService
class handles the heavy lifting when it comes to managing orders. Let’s break down how it works:
Key Responsibilities
-
createOrder(OrderRequest request)
:- This method is all about creating a new order. It checks if the package is available, grabs the details, reserves the quota, and saves the order to the database with an initial status of
RESERVED
.
- This method is all about creating a new order. It checks if the package is available, grabs the details, reserves the quota, and saves the order to the database with an initial status of
-
processPayment(String orderId, PaymentCallback callback)
:- Here, the payment gets handled. The system verifies the payment details, updates the order, puts the payment in escrow, and starts the service activation process. If something goes wrong, it gracefully manages failures.
-
verifyActivation(Order order)
:- This method double-checks if the service activation went smoothly. It tries up to three times, and if it still fails, the system falls back to handle the failure.
-
completeOrder(Order order)
:- Once everything checks out, this method finalizes the order. It releases the escrow funds to the provider, updates the status, and notifies the user about the success.
-
handleActivationFailure(Order order)
:- If activation fails, this method ensures the customer gets a refund and a notification about what went wrong.
-
getOrder(String orderId)
:- This straightforward method retrieves an order by its ID. If the order doesn’t exist, it throws a specific exception.
Why It Works
- It ensures transactions are either completed or rolled back, thanks to its transactional nature.
- With clear error handling and retries, it’s robust enough to handle real-world hiccups.
- Notifications keep users in the loop at every step.
This service is the backbone of the order management process, tying everything together for a seamless user experience.
// Order Controller
@RestController
@RequestMapping("/api/orders")
@Slf4j
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity createOrder(@Valid @RequestBody OrderRequest request) {
log.info("Received order creation request for package: {}", request.getPackageId());
try {
Order order = orderService.createOrder(request);
return ResponseEntity.ok(order);
} catch (QuotaNotAvailableException e) {
log.warn("Quota not available for package: {}", request.getPackageId());
return ResponseEntity.status(HttpStatus.CONFLICT).build();
} catch (Exception e) {
log.error("Error creating order", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/callback")
public ResponseEntity handlePaymentCallback(
@Valid @RequestBody PaymentCallback callback) {
log.info("Received payment callback for order: {}", callback.getOrderId());
try {
orderService.processPayment(callback.getOrderId(), callback);
return ResponseEntity.ok().build();
} catch (OrderNotFoundException e) {
log.warn("Order not found: {}", callback.getOrderId());
return ResponseEntity.notFound().build();
} catch (PaymentVerificationException e) {
log.warn("Payment verification failed for order: {}", callback.getOrderId());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
} catch (Exception e) {
log.error("Error processing payment callback", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/{orderId}")
public ResponseEntity getOrder(@PathVariable String orderId) {
log.info("Retrieving order: {}", orderId);
try {
Order order = orderService.getOrder(orderId);
return ResponseEntity.ok(order);
} catch (OrderNotFoundException e) {
log.warn("Order not found: {}", orderId);
return ResponseEntity.notFound().build();
}
}
}
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.
Key Endpoints
-
POST /api/orders
(createOrder):- This endpoint handles creating a new order.
- Here's what happens:
- It takes in an
OrderRequest
from the client. - Calls
OrderService.createOrder
to process the request and create the order. - Sends back:
- A
200 OK
response with the newly created order if all goes well. - A
409 Conflict
if the package quota is unavailable. - A
500 Internal Server Error
for any unexpected issues.
- A
- It takes in an
-
POST /api/orders/callback
(handlePaymentCallback):- This one processes payment updates sent by the payment gateway.
- Here's the flow:
- It receives a
PaymentCallback
with all the payment details. - Calls
OrderService.processPayment
to handle the payment and update the order status. - The possible responses are:
-
200 OK
if the payment is successfully handled. -
404 Not Found
if the order ID provided doesn’t exist. -
422 Unprocessable Entity
if there’s a mismatch in payment verification. -
500 Internal Server Error
for anything unexpected.
-
- It receives a
-
GET /api/orders/{orderId}
(getOrder):- This endpoint fetches the details of a specific order by its ID.
- Here's how it works:
- It calls
OrderService.getOrder
to retrieve the order. - Returns:
- A
200 OK
response with the order details if found. - A
404 Not Found
if the order ID doesn’t match any records.
- A
- It calls
Features
- Separation of Concerns: The
OrderController
delegates all business logic to theOrderService
, keeping things clean and focused. - Validation: Request payloads are validated using the
@Valid
annotation to ensure the data coming in meets expectations. - Error Handling:
- Provides specific and helpful responses for common issues, like unavailable quotas or missing orders.
- Logs any issues to make debugging easier.
- Logging: Tracks key events like incoming requests, errors, and order details for better visibility.
This controller ensures that the client and backend communicate seamlessly, making order management as smooth as possible.
Conclusion
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:
- Use event-driven architecture to make the system more flexible and scalable.
- Add message queue-based processing to handle lots of transactions smoothly.
- Explore advanced caching strategies to speed things up and reduce dependency on external APIs.
- Consider distributed system patterns for easier scaling and better reliability.
- Implement circuit breakers to handle third-party service hiccups gracefully.
- Set up monitoring and alerts to catch issues early and fix them quickly.
- Strengthen security measures to protect users and their data.
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: