Implement a versioned recipe management system
Company: Coinbase
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Online Assessment
Quick Answer: This question evaluates data-structure design and state-management competencies, covering CRUD, case-insensitive search/sort semantics, user management, and versioning/rollback for an in-memory Recipe Management System in the Coding & Algorithms domain.
Part 1: Level 1 - Basic CRUD for Recipes
Constraints
- 0 <= len(operations) <= 100000
- recipeId is a non-empty string with length at most 64
- name is a string with length at most 256
- 0 <= size <= 1000000000
- fields only needs to support the keys name and size
Examples
Input: ([['addRecipe', 'r1', 'Pancakes', 3], ['getRecipe', 'r1'], ['updateRecipe', 'r1', {'size': 4}], ['getRecipe', 'r1'], ['deleteRecipe', 'r1'], ['getRecipe', 'r1']],)
Expected Output: [True, {'recipeId': 'r1', 'name': 'Pancakes', 'size': 3}, True, {'recipeId': 'r1', 'name': 'Pancakes', 'size': 4}, True, None]
Explanation: The recipe is added, retrieved, updated, retrieved again, deleted, and then is no longer found.
Input: ([['addRecipe', 'r1', 'Soup', 2], ['addRecipe', 'r1', 'Salad', 1], ['getRecipe', 'r1']],)
Expected Output: [True, False, {'recipeId': 'r1', 'name': 'Soup', 'size': 2}]
Explanation: Duplicate recipe IDs are rejected, so the original recipe remains unchanged.
Hints
- Use a hash map keyed by recipeId so that CRUD operations are average O(1).
- When returning a recipe, return a copy rather than the internal object to avoid accidental mutation.
Part 2: Level 2 - Search and List Recipes
Constraints
- 0 <= len(operations) <= 100000
- recipeId is a non-empty string with length at most 64
- name and query are strings with length at most 256
- 0 <= size <= 1000000000
- sortBy is either name or size
Examples
Input: ([['addRecipe', 'r1', 'Apple Pie', 5], ['addRecipe', 'r2', 'banana Bread', 3], ['addRecipe', 'r3', 'Pineapple Tart', 4], ['searchRecipes', 'apple']],)
Expected Output: [True, True, True, [{'recipeId': 'r1', 'name': 'Apple Pie', 'size': 5}, {'recipeId': 'r3', 'name': 'Pineapple Tart', 'size': 4}]]
Explanation: The query apple matches Apple Pie and Pineapple Tart case-insensitively.
Input: ([['addRecipe', 'r2', 'banana', 2], ['addRecipe', 'r1', 'Apple', 2], ['addRecipe', 'r3', 'apple', 1], ['listRecipes', 'size'], ['listRecipes', 'name']],)
Expected Output: [True, True, True, [{'recipeId': 'r3', 'name': 'apple', 'size': 1}, {'recipeId': 'r1', 'name': 'Apple', 'size': 2}, {'recipeId': 'r2', 'name': 'banana', 'size': 2}], [{'recipeId': 'r1', 'name': 'Apple', 'size': 2}, {'recipeId': 'r3', 'name': 'apple', 'size': 1}, {'recipeId': 'r2', 'name': 'banana', 'size': 2}]]
Explanation: Size sorting is numeric ascending with recipeId tie-breaks. Name sorting is case-insensitive, so Apple and apple tie and are ordered by recipeId.
Hints
- Normalize names with lower() for both searching and name sorting.
- Keep the main storage as a dictionary, then sort a list of current recipe records only when searchRecipes or listRecipes is called.
Part 3: Level 3 - Add User Support
Constraints
- 0 <= len(operations) <= 100000
- recipeId and userId are non-empty strings with length at most 64
- recipe name, userName, and query are strings with length at most 256
- 0 <= size <= 1000000000
- sortBy is either name or size
Examples
Input: ([['addUser', 'u1', 'Ann'], ['addUser', 'u1', 'Annie'], ['addRecipe', 'r1', 'Stew', 4], ['listRecipes', 'name']],)
Expected Output: [True, False, True, [{'recipeId': 'r1', 'name': 'Stew', 'size': 4}]]
Explanation: The duplicate user is rejected, while recipe operations continue normally.
Input: ([['addRecipe', 'u1', 'Recipe Named Like User', 7], ['addUser', 'u1', 'Bob'], ['addUser', 'u1', 'Bobby'], ['getRecipe', 'u1']],)
Expected Output: [True, True, False, {'recipeId': 'u1', 'name': 'Recipe Named Like User', 'size': 7}]
Explanation: Recipe IDs and user IDs are separate namespaces.
Hints
- Keep users and recipes in separate dictionaries.
- Do not let addUser affect recipe search, list, get, update, or delete behavior.
Part 4: Level 4 - Version History and Rollback
Constraints
- 0 <= len(operations) <= 100000
- recipeId and userId are non-empty strings with length at most 64
- recipe name, userName, and query are strings with length at most 256
- 0 <= size <= 1000000000
- The number of versions is at most the number of successful mutating operations plus valid rollbacks plus one
Examples
Input: ([['addRecipe', 'r1', 'Soup', 2], ['updateRecipe', 'r1', {'name': 'Tomato Soup'}], ['getRecipe', 'r1'], ['rollback', 1], ['getRecipe', 'r1'], ['versionHistory']],)
Expected Output: [True, True, {'recipeId': 'r1', 'name': 'Tomato Soup', 'size': 2}, True, {'recipeId': 'r1', 'name': 'Soup', 'size': 2}, [{'versionId': 0, 'change': 'initial'}, {'versionId': 1, 'change': 'addRecipe:r1'}, {'versionId': 2, 'change': 'updateRecipe:r1'}, {'versionId': 3, 'change': 'rollback:1'}]]
Explanation: Rolling back to version 1 restores the original recipe name and creates version 3.
Input: ([['addRecipe', 'r1', 'Bread', 3], ['deleteRecipe', 'r1'], ['getRecipe', 'r1'], ['rollback', 1], ['getRecipe', 'r1']],)
Expected Output: [True, True, None, True, {'recipeId': 'r1', 'name': 'Bread', 'size': 3}]
Explanation: A deleted recipe can be restored by rolling back to a version where it existed.
Hints
- Represent each version as a node with a parent pointer and the single delta that created it.
- To rollback, undo deltas from the current version up to the lowest common ancestor with the target, then apply deltas down to the target.