Your task is to implement an in-memory recipe management system. This problem consists of 4 levels that progressively add complexity:
Level 1: Basic CRUD operations (create, read, update, delete)
Level 2: Search functionality and sorting by ingredient count
Level 3: User authentication and permission validation
Level 4: Version history and rollback capabilities
Subsequent levels are unlocked when the current level is correctly solved. You always have access to the data for the current and all previous levels.
Time Limit: 90 minutes
Initially, the recipe manager does not contain any recipes. Implement operations to allow recipe creation, retrieval, updates, and deletion.
add_recipe
add_recipe(self, recipe_id: str, name: str, ingredients: list[str]) -> bool
Creates a new recipe with the given identifier, name, and list of ingredients
Returns True if the recipe was successfully created
Returns False if:
A recipe with that id already exists
A recipe with that name already exists (case-insensitive comparison)
Important: Recipe names must be unique in a case-insensitive manner. For example, if "Pasta Carbonara" exists, attempting to add "pasta carbonara" or "PASTA CARBONARA" should fail.
get_recipe
get_recipe(self, recipe_id: str) -> dict | None
Retrieves the recipe with the given identifier
Returns the recipe data as a dictionary containing id, name, and ingredients
Returns None if the recipe doesn't exist
update_recipe
update_recipe(self, recipe_id: str, name: str, ingredients: list[str]) -> bool
Updates an existing recipe with new name and ingredients
Returns True if the recipe was successfully updated
Returns False if:
The recipe doesn't exist
The new name conflicts with another recipe's name (case-insensitive comparison)
Important: The same case-insensitive uniqueness rule applies. A recipe can keep its current name (even with different casing), but cannot use a name that belongs to a different recipe.
delete_recipe
delete_recipe(self, recipe_id: str) -> bool
Deletes the recipe with the given identifier
Returns True if the recipe was successfully deleted
Returns False if the recipe doesn't exist
The recipe manager should support searching for recipes and returning results in a specific sorted order.
search_recipes
search_recipes(self, query: str) -> list[dict]
Searches for recipes where the name contains the query string (case-insensitive)
Returns a list of matching recipes sorted according to the following rules:
Primary sort: Number of ingredients in ascending order (fewer ingredients first)
Secondary sort: Recipe ID in ascending order (numerically, if IDs are numeric strings)
Returns an empty list if no recipes match
get_all_recipes
get_all_recipes(self) -> list[dict]
Returns all recipes in the system
Results are sorted using the same sorting rules as search_recipes
The recipe manager should now incorporate a user system. Certain operations require user validation before they can be performed.
add_user
add_user(self, user_id: str, username: str) -> bool
Creates a new user with the given identifier and username
Returns True if the user was successfully created
Returns False if a user with that id already exists
edit_recipe
edit_recipe(self, user_id: str, recipe_id: str, name: str, ingredients: list[str]) -> bool
Updates an existing recipe, but only if the user_id is valid
Returns True if the recipe was successfully updated
Returns False if:
The user_id doesn't exist (user validation fails)
The recipe_id doesn't exist
The new name conflicts with an existing recipe name (case-insensitive)
Note: The original update_recipe from Level 1 may still exist, but edit_recipe adds user validation on top of the update logic.
The recipe manager should maintain a version history for each recipe. Updates no longer overwrite existing data but instead create new versions.
edit_recipe (Updated)
edit_recipe(self, user_id: str, recipe_id: str, name: str, ingredients: list[str]) -> bool
Instead of overwriting the current recipe data, this operation now appends a new version to the recipe's history
Each version is numbered sequentially (version 1, version 2, etc.)
The most recent version is considered the "current" version
All previous validation rules still apply
get_recipe_history
get_recipe_history(self, recipe_id: str) -> list[dict] | None
Returns the complete version history for the specified recipe
Each entry in the list represents a version, ordered from oldest to newest
Returns None if the recipe doesn't exist
rollback_recipe
rollback_recipe(self, user_id: str, recipe_id: str, version: int) -> bool
Creates a new version that is a copy of the specified historical version
This does NOT delete any versions — it simply adds the old version's data as the newest version
Returns True if the rollback was successful
Returns False if:
The user_id doesn't exist
The recipe_id doesn't exist
The specified version doesn't exist
The rollback would cause a recipe name conflict (if the old version's name now conflicts with another recipe's current name)
Example:
Recipe has versions: v1("Pasta", [...]), v2("Spaghetti", [...]), v3("Linguine", [...])
rollback_recipe(user, recipe_id, 1) creates v4 which is a copy of v1
Result: v1, v2, v3, v4("Pasta", [...])
"PASTA", "Pasta", and "pasta" are all considered the same name
When checking for duplicates, compare names in a case-insensitive manner
The original casing provided by the user should be preserved when storing
When rolling back to an old version, check if that version's name conflicts with any OTHER recipe's current name
A recipe can always rollback to a previous version of itself (no self-conflict)
If the old version's name matches another recipe's current name (case-insensitive), the rollback should fail
Versions are numbered starting from 1
Each edit (including rollback) increments the version number
Version numbers are never reused or reset
Recipe IDs and User IDs are valid string identifiers
Recipe names are non-empty strings
Ingredients lists can be empty but are typically non-empty
All operations should have reasonable time complexity for the expected data size
⚠️ Disclaimer: This is a sample solution to help you get started. To properly prepare for the interview, you should think through the question yourself and develop your own solution. Use this as a reference to validate your approach and identify areas you might have missed.
Note: This is the complete solution for all 4 levels. In the actual assessment, you would implement incrementally, starting with Level 1 and adding functionality for each subsequent level. The edit_recipe method shown here implements Level 4 behavior (version appending); in Level 3, it would simply overwrite like update_recipe.
class RecipeManager:
def __init__(self):
self.recipes = {} # recipe_id -> recipe data with history
self.users = {} # user_id -> user data
self.name_to_id = {} # lowercase name -> recipe_id (for uniqueness check)
# ==================== Level 1: Basic CRUD ====================
def add_recipe(self, recipe_id: str, name: str, ingredients: list[str]) -> bool:
# Check if recipe ID already exists
if recipe_id in self.recipes:
return False
# Check if name already exists (case-insensitive)
if name.lower() in self.name_to_id:
return False
# Create recipe with version history
self.recipes[recipe_id] = {
'id': recipe_id,
'history': [{'name': name, 'ingredients': ingredients.copy()}]
}
self.name_to_id[name.lower()] = recipe_id
return True
def get_recipe(self, recipe_id: str) -> dict | None:
if recipe_id not in self.recipes:
return None
recipe = self.recipes[recipe_id]
current = recipe['history'][-1] # Get latest version
return {
'id': recipe_id,
'name': current['name'],
'ingredients': current['ingredients'].copy()
}
def update_recipe(self, recipe_id: str, name: str, ingredients: list[str]) -> bool:
if recipe_id not in self.recipes:
return False
# Check name conflict (case-insensitive), excluding self
existing_id = self.name_to_id.get(name.lower())
if existing_id is not None and existing_id != recipe_id:
return False
# Remove old name mapping
old_name = self.recipes[recipe_id]['history'][-1]['name']
del self.name_to_id[old_name.lower()]
# Update recipe (overwrite for Level 1-3)
self.recipes[recipe_id]['history'] = [{'name': name, 'ingredients': ingredients.copy()}]
self.name_to_id[name.lower()] = recipe_id
return True
def delete_recipe(self, recipe_id: str) -> bool:
if recipe_id not in self.recipes:
return False
# Remove name mapping
current_name = self.recipes[recipe_id]['history'][-1]['name']
del self.name_to_id[current_name.lower()]
# Delete recipe
del self.recipes[recipe_id]
return True
# ==================== Level 2: Search and Sort ====================
def _sort_recipes(self, recipes: list[dict]) -> list[dict]:
"""Sort by ingredient count (asc), then by ID (numeric asc)."""
def sort_key(r):
ingredient_count = len(r['ingredients'])
recipe_id = r['id']
# Sort numerically if ID is a digit string, otherwise alphabetically
if recipe_id.isdigit():
return (ingredient_count, 0, int(recipe_id), "")
else:
return (ingredient_count, 1, 0, recipe_id)
return sorted(recipes, key=sort_key)
def search_recipes(self, query: str) -> list[dict]:
results = []
query_lower = query.lower()
for recipe_id, recipe in self.recipes.items():
current = recipe['history'][-1]
if query_lower in current['name'].lower():
results.append({
'id': recipe_id,
'name': current['name'],
'ingredients': current['ingredients'].copy()
})
return self._sort_recipes(results)
def get_all_recipes(self) -> list[dict]:
results = []
for recipe_id, recipe in self.recipes.items():
current = recipe['history'][-1]
results.append({
'id': recipe_id,
'name': current['name'],
'ingredients': current['ingredients'].copy()
})
return self._sort_recipes(results)
# ==================== Level 3: User System ====================
def add_user(self, user_id: str, username: str) -> bool:
if user_id in self.users:
return False
self.users[user_id] = {'id': user_id, 'username': username}
return True
def edit_recipe(self, user_id: str, recipe_id: str, name: str, ingredients: list[str]) -> bool:
# Validate user
if user_id not in self.users:
return False
# Validate recipe exists
if recipe_id not in self.recipes:
return False
# Check name conflict (case-insensitive), excluding self
existing_id = self.name_to_id.get(name.lower())
if existing_id is not None and existing_id != recipe_id:
return False
# Remove old name mapping
old_name = self.recipes[recipe_id]['history'][-1]['name']
del self.name_to_id[old_name.lower()]
# Add new version (Level 4 behavior)
self.recipes[recipe_id]['history'].append({
'name': name,
'ingredients': ingredients.copy()
})
self.name_to_id[name.lower()] = recipe_id
return True
# ==================== Level 4: Version Control ====================
def get_recipe_history(self, recipe_id: str) -> list[dict] | None:
if recipe_id not in self.recipes:
return None
history = []
for i, version in enumerate(self.recipes[recipe_id]['history'], 1):
history.append({
'version': i,
'name': version['name'],
'ingredients': version['ingredients'].copy()
})
return history
def rollback_recipe(self, user_id: str, recipe_id: str, version: int) -> bool:
# Validate user
if user_id not in self.users:
return False
# Validate recipe exists
if recipe_id not in self.recipes:
return False
history = self.recipes[recipe_id]['history']
# Validate version exists (1-indexed)
if version < 1 or version > len(history):
return False
# Get the old version data
old_version = history[version - 1]
# Check name conflict: if the old name differs from current name,
# check if it conflicts with another recipe
current_name = history[-1]['name']
if old_version['name'].lower() != current_name.lower():
existing_id = self.name_to_id.get(old_version['name'].lower())
if existing_id is not None and existing_id != recipe_id:
return False
# Remove current name mapping
del self.name_to_id[current_name.lower()]
# Add rolled-back version as new version
history.append({
'name': old_version['name'],
'ingredients': old_version['ingredients'].copy()
})
self.name_to_id[old_version['name'].lower()] = recipe_id
return True
manager = RecipeManager()
manager.add_recipe("1", "Spaghetti Bolognese", ["pasta", "ground beef", "tomato sauce"])
manager.add_recipe("2", "Caesar Salad", ["lettuce", "croutons", "parmesan"])
manager.add_recipe("3", "spaghetti bolognese", ["pasta"]) # Returns False (duplicate name)
results = manager.search_recipes("salad") # Returns Caesar Salad
manager.add_user("u1", "chef_john")
manager.edit_recipe("u1", "1", "Classic Bolognese", ["pasta", "beef", "tomatoes", "herbs"])
history = manager.get_recipe_history("1") # Shows all versions
manager.rollback_recipe("u1", "1", 1) # Rollback to original version