Using DynamoDB as a data store for an online shop
This use case talks about using DynamoDB as a data store for an online shop (or e-store).
Use case
An online store lets users browse through different products and eventually purchase them. Based on the generated invoice, a customer can pay using a discount code or gift card and then pay the remaining amount with a credit card. Purchased products will be picked from one of several warehouses and will be shipped to the provided address. Typical access patterns for an online store include:
-
Get customer for a given customerId
-
Get product for a given productId
-
Get warehouse for a given warehouseId
-
Get a product inventory for all warehouses by a productId
-
Get order for a given orderId
-
Get all products for a given orderId
-
Get invoice for a given orderId
-
Get all shipments for a given orderId
-
Get all orders for a given productId for a given date range
-
Get invoice for a given invoiceId
-
Get all payments for a given invoiceId
-
Get shipment details for a given shipmentId
-
Get all shipments for a given warehouseId
-
Get inventory of all products for a given warehouseId
-
Get all invoices for a given customerId for a given date range
-
Get all products ordered by a given customerId for a given date range
Entity relationship diagram
This is the entity relationship diagram (ERD) we'll be using to model DynamoDB as a data store for an online shop.
Access patterns
These are the access patterns we'll be considering when using DynamoDB as a data store for an online shop.
-
getCustomerByCustomerId -
getProductByProductId -
getWarehouseByWarehouseId -
getProductInventoryByProductId -
getOrderDetailsByOrderId -
getProductByOrderId -
getInvoiceByOrderId -
getShipmentByOrderId -
getOrderByProductIdForDateRange -
getInvoiceByInvoiceId -
getPaymentByInvoiceId -
getShipmentDetailsByShipmentId -
getShipmentByWarehouseId -
getProductInventoryByWarehouseId -
getInvoiceByCustomerIdForDateRange -
getProductsByCustomerIdForDateRange
Schema design evolution
Using NoSQL Workbench for DynamoDB , import AnOnlineShop_1.jsonAnOnlineShop and a new table called OnlineShop. Note that
we use the generic names PK and SK for the partition key and
sort key. This is a practice used in order to store different types of entities in the
same table.
Step 1: Address access pattern 1
(getCustomerByCustomerId)
Import AnOnlineShop_2.jsongetCustomerByCustomerId). Some entities do not have relationships to
other entities, so we will use the same value of PK and SK for
them. In the example data, note that the keys use a prefix c# in order to
distinguish the customerId from other entities that will be added later.
This practice is repeated for other entities as well.
To address this access pattern, a GetItem operation can be used with PK=customerId
and SK=customerId.
Step 2: Address access pattern 2
(getProductByProductId)
Import AnOnlineShop_3.jsongetProductByProductId) for the product entity. The
product entities are prefixed by p# and the same sort key attribute has
been used to store customerID as well as productID. Generic
naming and vertical
partitioning allows us to create such item collections for an effective
single table design.
To address this access pattern, a GetItem operation can be used with
PK=productId and SK=productId.
Step 3: Address access pattern 3
(getWarehouseByWarehouseId)
Import AnOnlineShop_4.jsongetWarehouseByWarehouseId) for the warehouse entity. We
currently have the customer, product, and
warehouse entities added to the same table. They are distinguished
using prefixes and the EntityType attribute. A type attribute (or prefix
naming) improves the model’s readability. The readability would be affected if we simply
stored alphanumeric IDs for different entities in the same attribute. It would be
difficult to tell one entity from the other in the absence of these identifiers.
To address this access pattern, a GetItem operation can be used with
PK=warehouseId and SK=warehouseId.
Base table:
Step 4: Address access pattern 4
(getProductInventoryByProductId)
Import AnOnlineShop_5.jsongetProductInventoryByProductId). warehouseItem entity is
used to keep track of the number of products in each warehouse. This item would normally
be updated when a product is added or removed from a warehouse. As seen in the ERD,
there is a many-to-many relationship between product and
warehouse. Here, the one-to-many relationship from product
to warehouse is modeled as warehouseItem. Later on, the
one-to-many relationship from warehouse to product will be
modeled as well.
Access pattern 4 can be addressed with a query on PK=ProductId and
SK begins_with “w#“.
For more information about begins_with() and other expressions that can
be applied to sort keys, see Key Condition
Expressions.
Base table:
Step 5: Address access patterns 5
(getOrderDetailsByOrderId) and 6
(getProductByOrderId)
Add some more customer, product, and warehouse
items to the table by importing AnOnlineShop_6.jsonorder that
can address access patterns 5 (getOrderDetailsByOrderId) and 6
(getProductByOrderId). You can see the one-to-many relationship between
order and product modeled as orderItem entities.
To address access pattern 5 (getOrderDetailsByOrderId), query the table
with PK=orderId. This will provide all information about the order
including customerId and ordered products.
Base table:
To address access pattern 6 (getProductByOrderId), we need to read
products in an order only. Query the table with PK=orderId and
SK begins_with “p#” to accomplish this.
Base table:
Step 6: Address access pattern 7
(getInvoiceByOrderId)
Import AnOnlineShop_8.jsoninvoice entity to the order item collection to handle access pattern 7
(getInvoiceByOrderId). To address this access pattern, you can use a
query operation with PK=orderId and SK begins_with
“i#”.
Base table:
Step 7: Address access pattern 8
(getShipmentByOrderId)
Import AnOnlineShop_9.jsonshipment entities to the order item collection to address access pattern 8
(getShipmentByOrderId). We are extending the same vertically
partitioned model by adding more types of entities in the single table design. Notice
how the order item collection contains the different
relationships that an order entity has with the shipment,
orderItem, and invoice entities.
To get shipments by orderId, you can perform a query operation with
PK=orderId and SK begins_with “sh#”.
Base table:
Step 8: Address access pattern 9
(getOrderByProductIdForDateRange)
We created an order item collection in the previous
step. This access pattern has new lookup dimensions (ProductID and
Date) which requires you to scan the whole table and filter out
relevant records to fetch targeted items. In order to address this access pattern, we'll
need to create a global secondary index (GSI). Import AnOnlineShop_10.jsonorderItem data from several order item collections. The data now has
GSI1-PK and GSI1-SK which will be GSI1’s
partition key and sort key, respectively.
DynamoDB automatically populates items which contain a GSI’s key attributes from the table to the GSI. There is no need to manually do any additional inserts into the GSI.
To address access pattern 9, perform a query on GSI1 with
GSI1-PK=productId and GSI1SK between (date1,
date2).
Base table:
GSI1:
Step 9: Address access patterns 10
(getInvoiceByInvoiceId) and 11
(getPaymentByInvoiceId)
Import AnOnlineShop_11.jsongetInvoiceByInvoiceId) and 11 (getPaymentByInvoiceId),
both of which are related to invoice. Even though these are two different
access patterns, they are realized using the same key condition. Payments
are defined as an attribute with the map data type on the invoice
entity.
Note
GSI1-PK and GSI1-SK is overloaded to store information
about different entities so that multiple access patterns can be served from the
same GSI. For more information about GSI overloading, see Overloading Global Secondary Indexes in DynamoDB.
To address access pattern 10 and 11, query GSI1 with
GSI1-PK=invoiceId and GSI1-SK=invoiceId.
GSI1:
Step 10: Address access patterns 12
(getShipmentDetailsByShipmentId) and 13
(getShipmentByWarehouseId)
Import AnOnlineShop_12.jsongetShipmentDetailsByShipmentId) and 13
(getShipmentByWarehouseId).
Notice that shipmentItem entities are added to the order item collection on the base table in order to be able
to retrieve all details about an order in a single query operation.
Base table:
The GSI1 partition and sort keys have already been used to model a
one-to-many relationship between shipment and shipmentItem. To
address access pattern 12 (getShipmentDetailsByShipmentId), query
GSI1 with GSI1-PK=shipmentId and
GSI1-SK=shipmentId.
GSI1:
We’ll need to create another GSI (GSI2) to model the new one-to-many
relationship between warehouse and shipment for access pattern
13 (getShipmentByWarehouseId). To address this access pattern, query
GSI2 with GSI2-PK=warehouseId and GSI2-SK
begins_with “sh#”.
GSI2:
Step 11: Address access patterns 14
(getProductInventoryByWarehouseId) 15
(getInvoiceByCustomerIdForDateRange), and 16
(getProductsByCustomerIdForDateRange)
Import AnOnlineShop_13.jsongetProductInventoryByWarehouseId),
query GSI2 with GSI2-PK=warehouseId and GSI2-SK
begins_with “p#”.
GSI2:
To address access pattern 15 (getInvoiceByCustomerIdForDateRange), query
GSI2 with GSI2-PK=customerId and GSI2-SK between
(i#date1, i#date2).
GSI2:
To address access pattern 16 (getProductsByCustomerIdForDateRange), query
GSI2 with GSI2-PK=customerId and GSI2-SK between
(p#date1, p#date2).
GSI2:
Note
In NoSQL Workbench, facets represent an application's different data access patterns for DynamoDB. Facets give you a way to view a subset of the data in a table, without having to see records that don't meet the constraints of the facet. Facets are considered a visual data modeling tool, and don't exist as a usable construct in DynamoDB as they are purely an aid for modeling access patterns.
Import AnOnlineShop_facets.json
All access patterns and how the schema design addresses them are summarized in the table below:
| Access pattern | Base table/GSI/LSI | Operation | Partition key value | Sort key value |
|---|---|---|---|---|
| getCustomerByCustomerId | Base table | GetItem | PK=customerId | SK=customerId |
| getProductByProductId | Base table | GetItem | PK=productId | SK=productId |
| getWarehouseByWarehouseId | Base table | GetItem | PK=warehouseId | SK=warehouseId |
| getProductInventoryByProductId | Base table | Query | PK=productId | SK begins_with "w#" |
| getOrderDetailsByOrderId | Base table | Query | PK=orderId | |
| getProductByOrderId | Base table | Query | PK=orderId | SK begins_with "p#" |
| getInvoiceByOrderId | Base table | Query | PK=orderId | SK begins_with "i#" |
| getShipmentByOrderId | Base table | Query | PK=orderId | SK begins_with "sh#" |
| getOrderByProductIdForDateRange | GSI1 | Query | PK=productId | SK between date1 and date2 |
| getInvoiceByInvoiceId | GSI1 | Query | PK=invoiceId | SK=invoiceId |
| getPaymentByInvoiceId | GSI1 | Query | PK=invoiceId | SK=invoiceId |
| getShipmentDetailsByShipmentId | GSI1 | Query | PK=shipmentId | SK=shipmentId |
| getShipmentByWarehouseId | GSI2 | Query | PK=warehouseId | SK begins_with "sh#" |
| getProductInventoryByWarehouseId | GSI2 | Query | PK=warehouseId | SK begins_with "p#" |
| getInvoiceByCustomerIdForDateRange | GSI2 | Query | PK=customerId | SK between i#date1 and i#date2 |
| getProductsByCustomerIdForDateRange | GSI2 | Query | PK=customerId | SK between p#date1 and p#date2 |
Online shop final schema
Here are the final schema designs. To download this schema design as a JSON file,
see DynamoDB Design Patterns
Base table
GSI1
GSI2
Using NoSQL Workbench with this schema design
You can import this final schema into NoSQL Workbench, a visual tool that provides data modeling, data visualization, and query development features for DynamoDB, to further explore and edit your new project. Follow these steps to get started:
-
Download NoSQL Workbench. For more information, see Download NoSQL Workbench for DynamoDB.
-
Download the JSON schema file listed above, which is already in the NoSQL Workbench model format.
-
Import the JSON schema file into NoSQL Workbench. For more information, see Importing an existing data model.
-
Once you've imported into NOSQL Workbench, you can edit the data model. For more information, see Editing an existing data model.
-
To visualize your data model, add sample data, or import sample data from a CSV file, use the Data Visualizer feature of NoSQL Workbench.