See PRD.
Definitions:
product
- Physical items like coffee, toilet paper, etc that a user can add to their delivery.bundle
- This is collection (unordered list) ofproducts
that a user gets a delivery on. -topProducts
- This is an ordered list of top 5 products that are most frequently added to a bundle.recommendedProducts
- This is an ordered list oftopProducts
we recommend to a given user to add to their bundle. This list excludes the products that the user has in the bundle.
As a user, I can
- Log in
- See my bundle of up to 5 items and a list of up to 5 recommended products list side-by-side (desktop) or top-bottom (mobile)
- Remove a product from my bundle, see both the bundle list and the recommended products list update.
- Select a product from the recommended list to add to my bundle, see both lists update.
- Click a button to log out and be redirected to the login page.
- (server) provide resources to the clients via various REST endpoints.
- (server) Compile top products for a user. This requires some server logic and - a simple data structure and algo problem.
- (client) Responsive layout.
- (client) Both lists update whenever a products is added/removed from a list. This should be achieved without a page refresh because it would be a frustrating user experience.
- Real login - For this project, we will just simulate the experience of logging in by entering a user id and click submit. Don't worry about implementing a real authentication service.
Typescript and ESLint In a real project, having TS and better linting will improve maintainability and ease of feature development as the codebase grows. But for this project, since the boilerplate starter is not already set up with TS and linting, it would require extra configuration and setup that is not worth the time for this takehome project.
Server-client interaction in logging in Since we are focused on implementing happy path with a fake log-in, let's not worry about validating that user id provided in the login form is a real user id that exists in the database. That would require creating a separate endpoint and making a separate request. The client can just assume that user id is valid and upon submit, persist the user id in app state and then redirect the user to the page that uses the persisted user id to make the requests to the server to get the bundle and recommended products lists.
Recommended List Caching. This is an important to have in the real world project because it can take the server some time to compile a recommendation list as it has to look at all the bundles from all the user. This would result in unacceptable latency for the end-user and unacceptable server cost for the company. However, for this takehome project, the server can compile the list in a reasonable amount of time because the number of users and products are small, so let's not worry about caching for now.
Expanded definition of popularity The recommended list is sorted by popularity amongst other users. During the design review, we talked about the definition of "popularity" and clarified that for V0 of this project, we can make the simplification that popularity is calculated based on frequency of occurrence across all the bundles. But in the future, we may want to have a fancier way of calculating popularity (e.g., based on a weighted sum approach), if we allow people to star the products in their bundles or rate these products (collaborative filtering).
API Versioning Even for internal APIs, it's a good idea to version the APIs so that we can make breaking changes to the API without breaking the clients. But for this project, let's not worry about versioning.
Error Handling In real life, we need to handle all 400 and 500 level errors which could result from invalid user input (e.g., user id not found), server errors, network errors etc (request failed to send). But for this project, let's not worry about client-side and server-side error handling and focus on getting the happy path to work.
Accessibility and Internationalization Important for real projects, especially if we are building a consumer application but let's not worry about them for this takehome.
We need to add some endpoints to the REST API to support the following calls:
- Query recommended products list for user
- Query bundle list for user
- Mutate bundle list for user
- Mutate recommended products list for user
Considerations
- Endpoint naming. A design principle I follow is unsurprising APIs are better. Use consistent, idiomatic, and familiar naming conventions and API design patterns. For example, we can use
GET /users/:userId/bundle
to get the bundle list, then we should useGET /users/:userId/recommended
to get the recommended products list.
Develop a simple algorithm to compile the top-5 recommended products list for a given user (KISS for this project, don't over-engineer it):
- Retrieve everyone else's bundles (use sql query
select * from user_products where user_id <> {userId}
). The list should look like this:
[
{
"id": 642,
"user_id": 4,
"product_id": 6
},
// ...
]
- Compute the recommended list by traversing the list above to build an occurrence dictionary (product_id -> count).
- Use the dictionary to create a sorted list of products by count in descending order.
- Return the top 5 products.
We can use some fancy data structures like max heap or something to improve space and runtime when creating that top-5 list but doing it the inefficient way is good enough for the small input.
Responsiveness Use flex box for the layout. Have media-breakpoint to make it mobile responsive.
Frontend Architecture
We follow the single responsibility principle and separate the concerns of the components. Some components specialize in the business logic (e.g. App
, and BundlesScreen
) while other components specialize in rendering and UI (e.g. BundleList
and RecommendedList
).
Specifically, we can have the following components:
App
is the top level component to persist the user id and call two child componentsLoginScreen
andBundlesScreen
based on the existence of a user id in its state.LoginScreen
renders the login form and sets the user id in the parent.BundlesScreen
retrieves the top-5 bundles list and the top-5 recommended list based on the user id that it's passed. It delegates the rendering of the two lists to two child componentsBundleList
andRecommendedList
. It will also pass the child components two setters to update the two lists.BundleList
renders all the products passed to it byBundleScreen
. Under each product, it displays a red "REMOVE" button.RecommendedList
renders all the products passed to it byBundleScreen
. Under each product, it displays a green "ADD" button.Product
renders a single product with a picture, name, price, and a price. This is a reusable component betweenBundleList
andRecommendedList
.
We will leverage Postman and mock data to enable parallel development and unit testing client-side and server-side.
We will do unit testing on the server side using the mock data we collected from making Postman requests to fetch everything from the database.
To ensure exhaustive testing, we should develop our test cases to ensure we handle these situations:
- no overlap between the user's bundle and the other users' bundles. This can happen when the user is new and has not added any products to their bundle yet.
- partial overlap
- complete overlap
Use Chrome DevTool to test mobile responsiveness on different screen sizes and different phones.
For desktop responsiveness, test:
- Really wide screen on my big monitor
- Narrow screen by resizing the browser window
For mobile responsiveness, we can select many iOS and Android in the Chrome DevTool. Test on different screen sizes to make sure it's awesome for the most common cases (most commonly used devices) and acceptable (usable) for the edge cases:
- Smallest screen size (iPhone 5)
- Largest screen size (iPad)
- Spot check a few in between, especially the most commonly used iOS and Android devices
Screenshots will be provided in the final PR showing how it looks on different screen sizes.
Backend/Frontend integration testing will be done before PR'ing to master. We will use the UI to test all the happy path and extreme cases, including:
- Use the red button to remove everything from the bundle list until the bundle is empty.
- Use the green button to add everything from the recommended list until the recommended list is empty.
A screen recording will be provided in the final PR showing this.
Test everything stated in the User Story. Screen recording will be provided in the final PR.
Specify number of products to get back
Should the number of products to get back from each list be a parameter to the API endpoints? Or we hardcode it to be 5 either on the server-side or client-side?
For the bundle list, I don't expect this to be a huge number. So a simple approach is for the server to return all the products in the bundle list to the client and client can take the top 5.
On the other hand, the recommended list can be huge. We'll get a network timeout if the server provides a huge list to the client. The server should impose a limit on the number of items it send back.
Another consideration is how much we trust the client to make a proper request? If we trust the client, we can have the client send a parameter to the server to indicate how many products it wants back. If we don't trust the client, we should prevent improper use of the API by hardcoding the number of products to get back on the server-side.