Questions/E-Commerce Site (e.g. Amazon)

E-Commerce Site (e.g. Amazon)

Hard
40 mins
Report an issue

Question

Design an e-commerce website that allows users to browse products and purchase them.

Product Listing Page Example

Product Details Page Example

Cart Page Example

Checkout Page Example

Real-life Examples


Requirements Exploration

What are the core features to be supported?

  • Browsing products.
  • Adding products to cart.
  • Checking out successfully.

What are the pages in the website?

  • Product listing page (PLP)
  • Product details page (PDP)
  • Cart page
  • Checkout page

What product details will be shown on the PLPs and PDPs?

  • PLPs: Product name, Product image, Price.
  • PDPs: Product name, Product images (multiple), Product description, Price.

How does the user demographics look like?

International users: US, Asia, Europe, etc.

What are the non-functional requirements?

Each page should load under 2 seconds. Interactions with page elements should be quick as well.

What devices will this component be used on?

All possible devices: laptop, tablets, mobile, etc.

Do users have to be signed in to purchase?

Users can purchase as a guest without being signed in.


Architecture/High-level Design

E-Commerce Website Architecture Diagram

Component Responsibilities

  • Server: Provides HTTP APIs to fetch products data, cart items, modify the carts and create orders.
  • Controller: Controls the flow of data within the application and makes network requests to the server.
  • Client Store: Stores data needed across the whole application. Since each page there are many pages, a client store is useful for sharing data across sections of a page and across pages.
  • Pages:
    • Product List: Displays a list of product items the user can add to cart.
    • Product Details: Displays details of a single product along with additional details.
    • Cart: Displays added cart items and allows changing of quantity and deleting added items.
    • Checkout: Displays address and payment forms to the user has to complete in order to place the order.

Server-side Rendering or Client-side Rendering?

Firstly let's understand what the two terms mean:

  • Server-side Rendering (SSR): The traditional way of building websites where the server fetches all necessary data, uses them to create the final markup and sends down the HTML needed every time a user visits a page. Most of the work is done on the server.
  • Client-side Rendering (CSR): The server sends down initial HTML which contains the JavaScript to bootstrap the application. The client then fetches the necessary data, combines it with templates and creates the final page, all within the browser. CSR is typically used with Single-page Application models and subsequent navigation don't require a full page refresh. Most of the work is done on the client.

Benefits of SSR:

  • Performance is generally better, First Contentful Paint score is high and SSR-ed pages appear faster than CSR.
  • Lower Cumulative Layout Shift score as the final HTML is already present.
  • SSR allows for personalization of pages (user-specific content) as opposed to static site generation. Personalization is an important factor for conversion as e-commerce platforms scale up.

Downsides of SSR:

  • Page transitions are slower because the entire page has to be constructed on the server for every request.

SEO is important for e-commerce websites, hence SSR should be a priority.

Single-page Applications (SPA) or Multi-page Applications (MPA)?

SPAs by default use CSR and MPAs by default use SSR. CSR and SSR are two ends of the rendering spectrum and somewhere in the middle lies a hybrid mode called universal rendering (or SSR with hydration) where the server renders the full HTML but after that, rendering and navigation becomes client-side.

For most sites using popular UI libraries like React and Vue, the page will need to be hydrated after initial load. Hydration also brings about the double data problem.

E-commerce sites in the wild and their architecture choices:

App TypeRendering
amazon.comMPASSR
ebay.comMPASSR
walmart.comSPASSR
flipkart.comSPASSR

All these e-commerce sites use SSR! This suggests the importance of using SSR for e-commerce sites.

Which application architecture should be used? The most important factor is that SSR is being used, whether SPA or MPA doesn't matter as much. As seen above, both are viable, as long as your website has good performance.

Real world technology choices to implement the following choices:

SSRCSR
SPANext.js, RemixCreate React App
MPATraditional HTML websitesThis combination isn't practical

The discussion below assumes that we'll be using a universal rendering approach of SSR + SPA.


Data Model

There are quite a number of entities involved in an e-commerce website due to the complexity of the user flow spanning across multiple pages.

EntitySourceBelongs ToFields
ProductServerProduct Listing Page, Product Details Pagename, description, unit_price, currency, primary_image, image_urls
CartServerClient Storeitems (list of CartItems), total_price, currency
CartItemServerClient Storequantity, product, price, currency
AddressDetailsUser input (Client)Checkout Pagename, country, street, city, etc.
PaymentDetailsUser input (Client)Checkout Pagecard_number, card_expiry, card_cvv

Few points for discussion:

  • The Cart entity belongs to the Client Store because some websites might want to show the number of cart items in the navbar or have a popup to allow users to quickly access the cart items and make modifications. If there's no such need then it's acceptable for the cart to belong to the Cart page.
  • Cart and CartItem have total_price and price fields respectively fetched as part of the server response instead of having the client compute the price (multiply the quantity by the unit_price) to give the flexibility of applying discounts due to bulk purchase or use of promo codes. The price computation logic is defined on the server, so the final price should be computed on the server and the client should rely on the server to calculate the total price instead of making its own calculations.
  • Since our website has an international audience, we should have localized prices in the user's currency, hence the currency field.

Interface Definition (API)

We need the following HTTP APIs:

  1. Product Information
    • Fetch products listing
    • Fetch a particular product's detail
  2. Cart Modification
    • Add a product to the cart
    • Change quantity of a product in the cart
    • Remove a product from the cart
  3. Complete the order

We will omit discussing about the APIs between the client components because the data format and functionalities are similar to the HTTP APIs.

Fetch Products Listing

FieldValue
HTTP MethodGET
Path/products
DescriptionFetches a list of products.

Parameters

ParameterTypeDescription
sizenumberNumber of results per page
pagenumberPage number to fetch
countrystringCountry for the user, determines the currency

Sample Response

{
"pagination": {
"size": 5,
"page": 2,
"total_pages": 4,
"total": 20
},
"results": [
{
"id": 123, // Product ID.
"name": "Cotton T-shirt",
"primary_image": "https://www.greatcdn.com/img/t-shirt.jpg",
"unit_price": 12,
"currency": "USD"
}
// ... More products.
]
}

We use offset-based pagination here as opposed to cursor-based pagination because:

  1. Having page numbers is useful for navigating between search results and jumping to specific pages.
  2. Product results do not suffer from the stale results issue that much as new products are not added that quickly/frequently.
  3. It's useful to know how many total results there are.

For a more in-depth comparison between offset-based pagination and cursor-based pagination, refer to the News Feed question.

Fetch Product Details

FieldValue
HTTP MethodGET
Path/products/{productId}
DescriptionFetches the details of a product.

Parameters

ParameterTypeDescription
productIdnumberProduct ID to be added
countrystringCountry for the user, determines the currency

Sample Response

{
"id": 123, // Product ID.
"name": "Cotton T-shirt",
"primary_image": "https://www.greatcdn.com/img/t-shirt.jpg",
"image_urls": [
"https://www.greatcdn.com/img/t-shirt.jpg",
"https://www.greatcdn.com/img/t-shirt-black.jpg",
"https://www.greatcdn.com/img/t-shirt-red.jpg"
],
"unit_price": 12,
"currency": "USD"
}

Add a Product to Cart

FieldValue
HTTP MethodPOST
Path/cart/add
DescriptionAdd a product to the cart

Parameters

ParameterTypeDescription
productIdnumberProduct ID to be added

Sample Response

The updated cart object is returned.

{
"id": 789, // Cart ID.
"total_price": 24,
"currency": "USD",
"items": [
{
"quantity": 2,
"price": 24,
"currency": "USD",
"product": {
"id": 123, // Product ID.
"name": "Cotton T-shirt",
"primary_image": "https://www.greatcdn.com/img/t-shirt.jpg"
}
}
]
}

Change Quantity of Product in Cart

FieldValue
HTTP MethodPUT
Path/cart/{productId}/quantity
DescriptionChange quantity of a product in the cart

Parameters

ParameterTypeDescription
productIdnumberID of product to be modified
quantitynumberNew quantity of the product

Sample Response

The updated cart object is returned.

{
"id": 789, // Cart ID.
"total_price": 24,
"currency": "USD",
"items": [
{
"quantity": 3,
"price": 36,
"currency": "USD",
"product": {
"id": 123, // Product ID.
"name": "Cotton T-shirt",
"primary_image": "https://www.greatcdn.com/img/t-shirt.jpg"
}
}
]
}

Remove Product from Cart

FieldValue
HTTP MethodDELETE
Path/cart/{productId}
DescriptionRemove a product from the cart

Parameters

ParameterTypeDescription
productIdnumberID of product to be modified

Sample Response

The updated cart object is returned.

{
"id": 789, // Cart ID.
"total_price": 0,
"currency": "USD",
"items": []
}

Place Order

FieldValue
HTTP MethodPOST
Path/order
DescriptionCreate an order from a cart

Parameters

ParameterTypeDescription
cartIDnumberID of the cart containing the items
address_detailsobjectObject containing address fields
payment_detailsobjectObject containing payment method fields (credit card)

Sample Response

The order object is returned upon successful order creation.

{
"id": 456, // Order ID.
"total_price": 36,
"currency": "USD",
"items": [
// ... Same items as per the cart.
],
"address_details": {
"name": "John Doe",
"country": "US",
"address": "1600 Market Street",
"city": "San Francisco"
// ... Other address fields.
},
"payment_details": {
// Only show the last 4 digits.
// We shouldn't be storing the credit card number
// unencrypted anyway.
"card_last_four_digits": "1234"
}
}

Notes

  • Depending on whether we want to optimize for returning users, we might want to save the address and payment details on the cart object so that people who abandoned the cart after filling up the checkout form but before placing the order and resume from where they left off without having to fill up the forms again.

Optimizations and Deep Dive

Performance

Performance is absolutely critical for e-commerce websites. Seemingly small performance improvements can lead to significant revenue and conversion increases. A study by Google and Deloitte showed that even a 0.1 second improvement in load times can improve conversion rates across the purchase funnel. web.dev by Google has a long list of case studies of how improving site performance led to improved conversions.

General Performance Tips

  • Code split JavaScript by routes/pages.
  • Split content into separate sections and prioritize above-the-fold content while lazy loading below-the-fold content.
  • Defer loading of non-critical JavaScript (e.g. code needed to show modals, dialogs, etc.).
  • Prefetch JavaScript and data needed for the next page upon hover of links/buttons.
    • Prefetch full product details needed by PDPs when users hover over items in PLPs.
    • Prefetch checkout page while on the cart page.
  • Optimize images with lazy loading and adaptive loading.
  • Prefetching top search results.

Core Web Vitals

Know the various core web vital metrics, what they are, and how to improve them.

  • Largest Contentful Paint (LCP): The render time of the largest image or text block visible within the viewport, relative to when the page first started loading.
    • Optimize performance – loading of JavaScript, CSS, images, fonts, etc.
  • First Input Delay (FID): Measures load responsiveness because it quantifies the experience users feel when trying to interact with unresponsive pages. A low FID helps ensure that the page is usable.
    • Reduce the amount of JavaScript needed to be executed on page load.
  • Cumulative Layout Shift (CLS): Measures visual stability because it helps quantify how often users experience unexpected layout shifts. A low CLS helps ensure that the page is delightful.
    • Include size attributes on images and video elements or reserve space for these elements using CSS aspect-ratio to reserve the required space for images while the images are loading. Use CSS min-height to minimize layout shifts while elements are lazy loaded.

Search Engine Optimization

SEO is extremely important for e-commerce websites as organic search is the primary way people discover products.

  • PDPs should have proper <title> and <meta> tags for description, keywords, and open graph tags.
  • Generate a sitemap.xml to tell crawlers the available pages of the website.
  • Use JSON structured data to help search engines understand the kind of content on your page. For the e-commerce case, the Product type would be the most relevant.
  • Use semantic markup for elements on the page, which also helps accessibility.
  • Ensure fast loading times to help the website rank better in Google search.
  • Use SSR for better SEO because search engines can index the content without having to wait for the page to be rendered (in the case of CSR).

Images

Images are one of the largest contributors to page size and serving optimized images is absolutely essential on e-commerce websites which are image heavy where every product has at least one image.

  • Use the WebP image format which is the most efficient image format that currently exists. eBay uses WebP format across all their web, Android and iOS apps. Ensure you are able to articulate on a high level why WebP format is superior.
  • Images should be hosted on a CDN.
  • Define the priority of the images and divide them into critical and non-critical assets.
  • Lazy load below-the-fold images.
    • Use <img loading="lazy"> for non-critical images.
  • Load critical images early.
    • Inline the image within the HTML as a data blob so that there's no need to make a separate HTTP request to fetch the image.
    • Using <link rel="preload"> so they download as soon as possible.
  • Adaptive loading of images, loading high-quality images for devices on fast networks and using lower-quality images for devices on slow networks.

Form Optimizations

Filling in forms is a huge part of the checkout flow and a very troublesome one at that. It is the last step of the checkout process and nailing a good checkout experience will greatly help in the conversion rate.

Completing forms is especially painful for mobile devices and extra attention has to be given to optimize forms for mobile. There are two kinds of forms to fill out during checkout, which are the shipping address forms and credit card forms.

Country-specific Shipping Address Forms

Different countries have different address formats. To optimize for global shipping, having localized address forms help greatly in improving conversions and that users do not drop off when filling out the address forms because they do not know how to understand certain fields. For example:

  • "ZIP Code"s are called "Postal Code"s in the United Kingdom.
  • There are no states in the Japan, only prefectures.
  • Different countries have their own postal/zip code formats and require different validation.

It is a hassle to have to find out these country-specific knowledge and also build these yourself, this is where services like Stripe Checkout come in helpful by providing a localized checkout form. Users will complete the rest of the payment flow on Stripe's platform.

Examples of Stripe checkout form for different countries

US UK
Stripe checkout for USStripe checkout for UK

Further reading: Frank's Compulsive Guide to Postal Addresses provides useful links and extensive guidance for address formats in over 200 countries.

Optimize Autofill

Filling up forms, especially long ones, are prone to typo errors. Most modern browsers have a feature called autofill, where they help users enter data faster and avoid filling up the same form data again by using values from similar forms filled previously.

Help users autofill their address forms by specifying the right type and autocomplete values for the form <input>s for the shipping address forms and credit card forms.

Shipping Address Form

FieldtypeautocompleteOthers
Nametextshipping nameautocorrect="off" spellcheck="false"
CountryUse <select>shipping countryN/A
Address line 1textshipping address-line1autocorrect="off" spellcheck="false
Address line 2textshipping address-line1autocorrect="off" spellcheck="false
Citytextshipping address-level2autocorrect="off" spellcheck="false
StateUse <select>shipping address-level1N/A
Postal Codetextshipping postal-codeautocorrect="off" spellcheck="false inputmode="numeric"

Credit Card Form

FieldtypeautocompleteOthers
Card Numbertextcc-numberautocorrect="off" spellcheck="false inputmode="numeric"
Card Expirytextcc-expautocorrect="off" spellcheck="false inputmode="numeric"
Card CVCtextcc-cscautocorrect="off" spellcheck="false inputmode="numeric"

Notes

  • inputmode="numeric" provides a hint to browsers for devices with onscreen keyboards to help them decide which keyboard to display. Unlike <input type="number">, inputmode="numeric" does not prevent users from typing non-numeric values, they simply affect the keyboard being shown. As these numeric fields are not related to quantity, the chevrons which appear when using <input type="number"> are not too helpful. <input type="text" inputmode="numeric"> also works with the maxlength/minlength/pattern attributes, but do not work with <input type="number>.

Read more about forms:

Alternative Ways of Address Input

Instead of making users fill up a form containing granular address fields, there are other approaches which may be easier, at the cost of engineering complexity or reliance on external services:

  1. Address search/autocomplete: Allow users to search for an address by typing in their street number and letting them select from a list of suggestions. This reduces typos and is generally faster. However, users should still be given the option to override some values in case none of the suggestions are correct, which can happen due to an outdated address database. Google Maps JavaScript API provides this via Place Autocomplete library.

  2. Selecting address location from a map: Open up a map and allow users to pinpoint a location on the map. This is less common for checkout addresses and more common for ridehailing applications.

Error Messages

Leverage client-side validation and clearly communicate any form errors. Connect the error message to the <input> via aria-describedby and use aria-live="assertive" for the error message.

<form>
<div>
<label for="name">Name</label>
<input
required
minlength="6"
type="text"
id="name"
name="name"
aria-describedby="name-error-message" />
<span
id="name-error-message"
aria-live="assertive"
class="name-error-message">
Name must have at least 6 characters!
</span>
</div>
<button>Submit</button>
</form>

Source: Help users find the error message for a form control | web.dev

Focus States

Make the currently focused form control visually different from the other form inputs to help users identify which element is being focused.

Best Practices for Payment and Address Forms

Read more about building good Payment forms and Address forms and the best practices on web.dev.

i18n

  • Have pages translated in the supported languages.
    • Set the lang attribute on the html tag (e.g. <html lang="zh-cn">) to tell browsers and search engines the language of the page which helps browsers offer a translation of the page.
    • Provide support for RTL languages by using CSS Logical Properties
  • Forms
    • Use a single form fields for names.
    • Allow for various address formats. This is covered in the section on address forms.

Read more:

Accessibility

  • Use semantic elements where possible: headings, buttons, links, inputs instead of styled <div>s.
  • <img> tags should have the alt attribute specified.
  • Building accessible forms has been covered in detail above. In summary:
    • <input>s should have associated <label>s.
    • <input>s are linked to their error messages via aria-describedby and error messages are announced with aria-live="assertive".
    • Use <input>s of the correct types and appropriate validation-related attributes like pattern, minlength, maxlength.
    • Visual order matches DOM order.
    • Make the currently focused form control obvious.

Reference: Accessibility | Forms | web.dev and WebAIM: Creating Accessible Forms

Security

Since payment details are highly sensitive, we have to make sure the website is secure:

  • Use HTTPS so that all communication with the server is encrypted and that other users on the same Wi-FI network cannot intercept and obtain any sensitive details.
  • Payment details submission API should not be using HTTP GET because the sensitive details will be included as a query string in the request URL which will get added to the browsing history which is potentially unsafe if the browser is shared by other users. Use HTTP POST or PUT instead.

Source: Security and privacy | web.dev

User Experience

  • Make the checkout page clean (e.g. minimal navbar and footer) and remove distractions to reduce bounce rate.
  • Allow persisting cart contents (either in database or cookies) as some people spend time researching and considering, only making the purchase during subsequent sessions. You don't want them to have to add all of the items to their cart again.
  • Make promo code fields less prominent so that people without promo codes will not leave the page to search the web for a promo code. Those who have a promo code beforehand will take the effort to find the promo code input field.

References

Previous
Autocomplete