Ensuring Checkout Integrity Multi-Tab E-Commerce Journeys: Handling Cart Modifications and Navigation Flow

Vipul Gupta
6 min readJan 17, 2025

--

We are building OneShop, an e-commerce platform for telco products.

Building and managing an e-commerce platform for telco products comes with unique challenges. These platforms must cater to a complex user journey, ensure high data integrity, and handle frequent modifications while providing a seamless experience. Below are the key challenges and considerations:

Key Problems in the Checkout Journey

  1. Step Skipping via URL Manipulation
  • Users bypass mandatory steps in the checkout process by directly altering the URL (e.g., navigating from the basket to the payment page without completing personal or shipping information).
  • This leads to incomplete data submission, inconsistent user journeys, and potential errors during order processing.

2. Multi-Tab Modifications Creating Inconsistencies

  • Users duplicate the checkout tab and perform conflicting actions across tabs. For example:
  • Modifying the basket in one tab (e.g., adding or removing items).
  • Continuing the checkout process in another tab without reflecting the changes made.
  • This results in stale data and invalid states, making it difficult to ensure accurate and seamless order placement.

3. Data Consistency Across Steps

  • Ensuring that user inputs (e.g., personal, shipping, or payment details) are captured and validated sequentially without skipping intermediate steps.
  • Discrepancies in sequential validation can result in missing critical information like addresses, shipping preferences, or payment confirmations.

4. Concurrent Cart Updates

  • Users sharing carts across multiple devices or sessions can create conflicting updates, where the checkout journey in one session invalidates the state in another. For example:
  • Device A updates the cart while Device B is at the payment stage.
  • The mismatch between cart states can lead to failed transactions or incorrect order details.

5. User Frustration Due to Soft Errors

  • Users encountering sudden redirects (e.g., being sent back to the basket) without clear messaging about what went wrong.
  • This can degrade user experience and reduce confidence in the checkout process.

6. Multi-Basket Scenarios Complicating Checkout

  • If a user adds items to create multiple active baskets, the system must handle and clearly define which basket is active in the checkout flow.
  • Handling multiple baskets during checkout can lead to confusion, especially when users try to proceed with a different basket than initially selected.

Solution Implemented

We identified the steps involved in the checkout journey and provided a ranking for each step:

BASKET("basket", 1),
PERSONAL_INFO("personalInfo", 2),
COMPANY_INFO("companyInfo", 2),
CONNECTION_SKIP("connectionSkip",3),
CONNECTION("connection",3),
BOOKING_APPOINTMENT("bookingAppointment", 3),
BOOKING_APPOINTMENT_SKIP("bookingAppointmentSkip", 3),
SHIPPING("shipping", 4),
SHIPPING_SKIP("shippingSkip", 4),
PAYMENT("payment", 5),
PAYMENT_SKIP("paymentSkip", 5),
BILLING("billing", 6),
BILLING_SKIP("billingSkip", 6),
ORDER_REVIEW("orderReview", 7);

1. Front-End Responsibilities

Sharing currentScreenStep:

  • For every API request during the user journey, the FE includes the currentScreenStep to indicate the current step in the sequence (e.g., personalInfo, shippingInfo).

Sharing completionStep:

  • The FE sends the completionStep only in the final API call of a particular screen/step.

Sharing lastModificationTimeStamp (LMT)

  • The FE stores the LastModificationTimeStamp (LMT) in session storage (tab-specific).
  • Every API request includes the LMT to ensure consistency with the backend data.

2. Back-End Responsibilities

The backend validates requests based on currentScreenStep, completionStep, and LastModificationTimeStamp (LMT) to ensure data integrity and prevent inconsistencies.

Scenarios:

  • Match: Proceed with request processing.
  • Mismatch: Reject the request and send an error response to the FE indicating stale cart data.
  1. Request Validation on Arrival:

Validate LMT:

  • The LMT sent by the FE is compared with the CHANGED_DATE stored in the database (ShoppingCart table).
private boolean isValidCurrentTimeStampProvided(String lastModificationTimeStamp,
ShoppingCart shoppingCart) {
boolean isValid = true;
if (Objects.isNull(shoppingCart) || Objects.isNull(shoppingCart.getAudit())
|| Objects.isNull(shoppingCart.getAudit().getChangedDate())
|| StringUtils.isEmpty(lastModificationTimeStamp)
|| lastModificationTimeStamp.equals("N/A")) {
isValid = false;
}
if (Objects.nonNull(shoppingCart) && Objects.nonNull(shoppingCart.getAudit())
&& Objects.nonNull(shoppingCart.getAudit().getChangedDate())
&& !StringUtils.equals(lastModificationTimeStamp,
String.valueOf(shoppingCart.getAudit().getChangedDate().toEpochSecond()))) {
isValid = false;
}
return isValid;
}

Validate currentScreenStep:

  • Check if the currentScreenStep is consistent with the expected journey sequence.
  • Reject requests if the user is attempting to skip steps.
private boolean isValidCurrentStepRequest(String currentStep, ShoppingCart shoppingCart) {
boolean isValid = true;
if (Objects.isNull(shoppingCart) || Objects.isNull(shoppingCart.getCompletionStep())) {
isValid = false;
}
if (Objects.nonNull(shoppingCart) && Objects.nonNull(shoppingCart.getCompletionStep())) {
int rankCurrentStep = CurrentScreenStep.of(currentStep).getRanking();
int rankCompletionStep = CurrentScreenStep
.of(shoppingCart.getCompletionStep().getCurrentScreenStep()).getRanking();
int diff = rankCurrentStep - rankCompletionStep;
if (diff > 1) {
isValid = false;
}
}
return isValid;
}

Post-Execution Logic:

  • AOP Aspect Logic (AfterReturning Advice):
  • After successfully processing the API request:
  1. Update the completionStep in the database with the value received from the FE.
  2. Fetch the updated LMT from the ShoppingCart table.
  3. Include the new LMT in the API response to the FE.
@AfterReturning("publicMethodInsideAClassMarkedWithRestController()")
public void updateCompletionStepAndReturnLMT() {
if (!Boolean.parseBoolean(HttpUtils.getHeaderValue(request, X_REQUEST_S_2_S_IDENTIFIER))
&& BffUtils.isMultiTabModificationValidationEnabled()) {
String completionScreenStep = request.getParameter(CommonConstants.COMPLETION_STEP);
String currentScreenStep = request.getHeader(CommonConstants.X_REQUEST_CURRENT_STEP);
String lastModificationTimeStampFromRequest =
request.getHeader(CommonConstants.X_REQUEST_CART_LAST_MODIFIED_TIMESTAMP);
if (StringUtils.isNotEmpty(completionScreenStep)
|| Objects.nonNull(CurrentScreenStep.of (currentScreenStep))
|| DEVICE_DETAIL.equals(currentScreenStep)){
ShoppingCart sc = BffUtils.getShoppingCart();
// check for back and forth of screen which are not modifying database by itself
if (StringUtils.isNoneEmpty(completionScreenStep, currentScreenStep,
lastModificationTimeStampFromRequest)) {
boolean completionStepSame = multiTabHandler.isCompletionStepAndLmtSameInDB(completionScreenStep,
lastModificationTimeStampFromRequest, sc);
if (completionStepSame) {
response.setHeader(CommonConstants.X_REQUEST_CART_LAST_MODIFIED_TIMESTAMP,
lastModificationTimeStampFromRequest);
return;
}
}
if (Objects.nonNull(CurrentScreenStep.of(currentScreenStep))
|| DEVICE_DETAIL.equals(currentScreenStep)) {
OffsetDateTime lastModificationTimeStamp = null;
if ((StringUtils.isNotEmpty(completionScreenStep)
|| CurrentScreenStep.BASKET.getCurrentScreenStep().equals(completionScreenStep)
|| DEVICE_DETAIL.equals(currentScreenStep)) && Objects.nonNull(sc)) {
sc.setCompletionStep(CurrentScreenStep.of(completionScreenStep));
lastModificationTimeStamp = multiTabHandler.updateShoppingCart(sc);
}
if (Objects.nonNull(lastModificationTimeStamp)) {
multiTabHandler.setHeaders(response,
String.valueOf(lastModificationTimeStamp.toEpochSecond()));
} else {
if (Objects.nonNull(sc) && Objects.nonNull(sc.getAudit())
&& Objects.nonNull(sc.getAudit().getChangedDate())) {
multiTabHandler.setHeaders(response,
String.valueOf(sc.getAudit().getChangedDate().toEpochSecond()));
}
}
}
}
}
}

Case 1 : Success

Tab 1 (Original) Journey with LMT Check:

Step 1                    Step 2                    Step 3                     Step 4                     Step 5
------------------->------------------->------------------->------------------->------------------->
"BASKET" "PERSONAL_INFO" "CONNECTION" "SHIPPING" "PAYMENT"
(currentStep) (currentStep) (currentStep) (currentStep) (currentStep)
(lastModTime) (lastModTime) (lastModTime) (lastModTime) (lastModTime)
(completionStep) (completionStep) (completionStep) (completionStep) (completionStep)

Step 1 Example:
{
"currentScreenStep": "basket",
"completionStep": "basket",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}

Step 2 Example:
{
"currentScreenStep": "personalInfo",
"completionStep": "personalInfo",
"lastModificationTimeStamp": "2024-01-17T12:05:00Z"
}

Step 3 Example:
{
"currentScreenStep": "connection",
"completionStep": "connection",
"lastModificationTimeStamp": "2024-01-17T12:10:00Z"
}

Step 4 Example:
{
"currentScreenStep": "shipping",
"completionStep": "shipping",
"lastModificationTimeStamp": "2024-01-17T12:15:00Z"
}

Step 5 Example:
{
"currentScreenStep": "payment",
"completionStep": "payment",
"lastModificationTimeStamp": "2024-01-17T12:20:00Z"
}

Case 2: Failure

Tab 1 (Original) and Tab 2 (Duplicate) Journey with LMT Check:

Tab 1 (Original)                                Tab 2 (Duplicate)
-------------------->---------------------------------------------->
"BASKET" (Step 1) → "PERSONAL_INFO" (Step 2) "BASKET" (Step 1) → "PERSONAL_INFO" (Step 2)
(currentStep) (currentStep)
(lastModTime) (lastModTime)
(completionStep) (completionStep)
--------------------------------------------------->------------------------------->
Step 1 Example: Step 1 Example:
{
"currentScreenStep": "basket", {
"completionStep": "basket", "currentScreenStep": "basket",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z" "completionStep": "basket",
} "lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}

Tab 2 Update:
{
"currentScreenStep": "personalInfo",
"completionStep": "personalInfo",
"lastModificationTimeStamp": "2024-01-17T12:05:00Z"
}

**At this point, the LMT on Tab 2 is updated (modified)** after changes are made.

Now, on **Tab 1** (Original):
{
"currentScreenStep": "personalInfo",
"completionStep": "personalInfo",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}

Tab 1 has an **older LMT** value ("2024-01-17T12:00:00Z"), while Tab 2 has an **updated LMT** ("2024-01-17T12:05:00Z").

--------------------------------------------------->------------------------------->
**LMT Mismatch Detected** **Invalid Cart Detected**
{ }
"currentScreenStep": "personalInfo" |
"completionStep": "personalInfo" |
"lastModificationTimeStamp": "2024-01-17T12:05:00Z" - LMT on Tab 2 is newer than Tab 1
}

Case 3: Failure

Tab 1 (Original) with URL manipulation by skipping step with currentScreenStep Check:

Tab 1 (Original)                              Tab 2  Manipulated(Duplicate)
-------------------->---------------------------------------------->
"BASKET" (Step 1) → "PERSONAL_INFO" (Step 2) "BASKET" (Step 1) → "SHIPPING" (Step 2)
(currentStep) (currentStep)
(lastModTime) (lastModTime)
(completionStep) (completionStep)
--------------------------------------------------->------------------------------->
Step 1 Example: Step 1 Example:
{
"currentScreenStep": "basket", {
"completionStep": "basket", "currentScreenStep": "SHIPPING",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z" "completionStep": "SHIPPING",
} "lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}


**At this point, the currentScreenStep on Tab 2 is updated (modified)** after changes are made in URL.

Now, on **Tab 2** :
{
"currentScreenStep": "SHIPPING",
"completionStep": "SHIPPING",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}

Tab 2 has an **new currentScreenStep ** value SHIPPING while BackEnd system has completionStep as Basket in database.

Ranking of Shipping = 4
Ranking of Basket = 1

CompletionStep(in db)- currentScreenStep(from FE)

Now the difference is 3, but the allowed difference is 1, So it will reject the reqeust.
--------------------------------------------------->------------------------------->
**CurrentScreenStep Mismatch Detected** **Invalid Cart Detected**
{ }
"currentScreenStep": "SHIPPING" |
"completionStep": "SHIPPING" |
"lastModificationTimeStamp": "2024-01-17T12:05:00Z"
}

Conclusion:

The OneShop e-commerce platform’s approach to multi-tab checkout integrity provides a highly reliable and robust solution that addresses common challenges in the e-commerce space. By implementing a comprehensive strategy that combines timestamp-based validation, step-by-step validation, and multi-tab synchronization, we ensure that users experience a seamless, error-free journey from cart to payment. This results in increased customer satisfaction, higher conversion rates, and a more reliable platform overall. The robust backend validation coupled with proactive error handling creates a frictionless experience, mitigating the risks associated with concurrent sessions and ensuring that data remains accurate and consistent across the entire checkout process.

--

--

Vipul Gupta
Vipul Gupta

No responses yet