Implement a versioned recipe management system
Company: Coinbase
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Take-home Project
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.
Input: ([['getRecipe', 'missing'], ['updateRecipe', 'missing', {'name': 'New'}], ['deleteRecipe', 'missing']],)
Expected Output: [None, False, False]
Explanation: Missing recipes cannot be retrieved, updated, or deleted.
Input: ([],)
Expected Output: []
Explanation: Edge case: no operations produces no results.
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.
Input: ([['addRecipe', 'r1', 'Curry', 5], ['addRecipe', 'r2', 'Soup', 2], ['updateRecipe', 'r2', {'name': 'Curry Soup'}], ['searchRecipes', 'CURRY'], ['deleteRecipe', 'r1'], ['listRecipes', 'name']],)
Expected Output: [True, True, True, [{'recipeId': 'r1', 'name': 'Curry', 'size': 5}, {'recipeId': 'r2', 'name': 'Curry Soup', 'size': 2}], True, [{'recipeId': 'r2', 'name': 'Curry Soup', 'size': 2}]]
Explanation: Search reflects updates, and listRecipes reflects deletions.
Input: ([['searchRecipes', 'x'], ['listRecipes', 'size']],)
Expected Output: [[], []]
Explanation: Edge case: searching or listing an empty recipe store returns an empty list.
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.
Input: ([['addRecipe', 'r1', 'Apple', 1], ['addUser', 'u2', 'Cara'], ['updateRecipe', 'r1', {'name': 'Apple Pie'}], ['searchRecipes', 'pie']],)
Expected Output: [True, True, True, [{'recipeId': 'r1', 'name': 'Apple Pie', 'size': 1}]]
Explanation: User creation does not interfere with updating or searching recipes.
Input: ([],)
Expected Output: []
Explanation: Edge case: no operations produces an empty result list.
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.
Input: ([['addUser', 'u1', 'Ann'], ['addUser', 'u1', 'Annie'], ['rollback', 99], ['versionHistory'], ['rollback', 0], ['addUser', 'u1', 'Annie'], ['versionHistory']],)
Expected Output: [True, False, False, [{'versionId': 0, 'change': 'initial'}, {'versionId': 1, 'change': 'addUser:u1'}], True, True, [{'versionId': 0, 'change': 'initial'}, {'versionId': 1, 'change': 'addUser:u1'}, {'versionId': 2, 'change': 'rollback:0'}, {'versionId': 3, 'change': 'addUser:u1'}]]
Explanation: Duplicate addUser and invalid rollback do not create versions. After rolling back to version 0, user u1 can be added again.
Input: ([['addRecipe', 'r1', 'A', 1], ['updateRecipe', 'r1', {'name': 'B'}], ['rollback', 1], ['updateRecipe', 'r1', {'name': 'C'}], ['rollback', 2], ['getRecipe', 'r1']],)
Expected Output: [True, True, True, True, True, {'recipeId': 'r1', 'name': 'B', 'size': 1}]
Explanation: This tests rollback across branches: after creating a new branch with name C, rollback to version 2 restores name B.
Input: ([['versionHistory'], ['rollback', 0], ['versionHistory']],)
Expected Output: [[{'versionId': 0, 'change': 'initial'}], True, [{'versionId': 0, 'change': 'initial'}, {'versionId': 1, 'change': 'rollback:0'}]]
Explanation: Edge case: rolling back to the initial version is valid even when the data is already empty.
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.