PracHub
QuestionsPremiumCoachesLearningGuidesInterview Prep

Quick Overview

Tests skills in data modeling and stateful class implementation, covering CRUD operations, case-insensitive string handling, recipe ID generation, list management, search and sort logic, and basic user/edit permissions.

  • medium
  • Ramp
  • Coding & Algorithms
  • Software Engineer

Implement a multi-level digital recipe manager

Company: Ramp

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Take-home Project

Implement a `RecipeManager` class for a digital recipe manager. The task is progressive (Levels 1–4). You can assume a single-process in-memory implementation. Correctness matters; efficiency is not required. Unless otherwise stated, all string comparisons for recipe names and ingredient matching are **case-insensitive**. ### Data model A recipe has: - `recipe_id`: formatted as `"recipe" + N`, where `N` is a sequential integer starting from 1 in creation order. - `name`: unique across all recipes (case-insensitive). - `ingredients`: list of strings (keep original order). - `steps`: list of strings (keep original order). When returning a recipe, represent it as: - `[name, ingredients_as_string, steps_as_string]` - `ingredients_as_string` is the ingredients joined by `","` (comma separator), and similarly for `steps_as_string`. If a method returns a list of recipe IDs, they are strings like `"recipe1"`, `"recipe2"`, etc. --- ## Level 1: Basic CRUD (no users) Implement: 1) `add_recipe(self, name: str, ingredients: list[str], steps: list[str]) -> str | None` - Add a new recipe and return its `recipe_id`. - If a recipe with the same name already exists (case-insensitive), return `None`. 2) `get_recipe(self, recipe_id: str) -> list[str]` - Return `[name, ingredients_as_string, steps_as_string]` for the recipe. - If the recipe does not exist, return an empty array `[]`. - Ingredients must be returned in the same order they were added. 3) `update_recipe(self, recipe_id: str, name: str, ingredients: list[str], steps: list[str]) -> bool` - Update the recipe fields. - Return `True` if successful. - Return `False` if the recipe does not exist, or if the new name conflicts with another existing recipe name (case-insensitive). 4) `delete_recipe(self, recipe_id: str) -> bool` - Delete the recipe. - Return `True` if it existed and was deleted; otherwise `False`. --- ## Level 2: Searching and sorting Implement: 5) `search_recipes_by_ingredient(self, ingredient: str) -> list[str]` - Return recipe IDs for recipes that contain the specified ingredient (case-insensitive match). - Sort results by: 1. number of ingredients ascending 2. `recipe_id` ascending (tie-breaker) 6) `list_recipes(self, sort_by: str) -> list[str]` - Return all recipe IDs sorted by `sort_by`, where `sort_by` is either: - `"name"`: lexicographic ascending by recipe name; tie-breaker `recipe_id` ascending - `"ingredient_count"`: ascending by number of ingredients; tie-breaker `recipe_id` ascending - If `sort_by` is invalid, default to sorting by `"name"`. --- ## Level 3: Users and edits Introduce users. 7) `add_user(self, user_id: str) -> bool` - Add a new user. - Return `True` if added; `False` if the user already exists. 8) `edit_recipe(self, user_id: str, recipe_id: str, new_name: str, new_ingredients: list[str], new_steps: list[str]) -> bool` - Any existing user may edit any recipe. - Enforce recipe-name uniqueness (case-insensitive). - Return `True` on success. - Return `False` if the user does not exist, the recipe does not exist, or the new name conflicts with another recipe name. --- ## Level 4: Version history and rollback Add version control for recipes. Definitions: - Recipes created by `add_recipe` have **no version history** until the first successful `edit_recipe` or `update_recipe`. - Every successful `edit_recipe` or `update_recipe` creates a new version entry. - Version entries are numbered starting at 1 and increase by 1 per entry. - Each version string format: - `<version>:<name>:<ingredients_as_string>:<steps_as_string>:<last_edited_by>` - For `edit_recipe`, `<last_edited_by>` is the `user_id`. - For `update_recipe`, `<last_edited_by>` is the literal string `"system"`. Implement: 9) `version_recipe(self, recipe_id: str) -> list[str]` - Return all versions for the recipe, sorted by version number ascending. - Return `[]` if the recipe does not exist or if it was never edited/updated (i.e., no version history). 10) `rollback_recipe(self, recipe_id: str, version: int) -> bool` - Roll back the current recipe state (name/ingredients/steps) to match the specified historical `version`. - Rolling back creates a **new** version entry appended to the history (history is immutable), with editor set to the literal string `"rollback"`. - Return `True` if successful. - Return `False` if the recipe does not exist, the requested version does not exist, or if rolling back would cause a name conflict with another existing recipe (case-insensitive).

Quick Answer: Tests skills in data modeling and stateful class implementation, covering CRUD operations, case-insensitive string handling, recipe ID generation, list management, search and sort logic, and basic user/edit permissions.

Part 1: Simulate Basic Recipe CRUD

Write `solution(operations)` to simulate a simple in-memory recipe manager. Each operation is one of: - `['add_recipe', name, ingredients, steps]` - `['get_recipe', recipe_id]` - `['update_recipe', recipe_id, name, ingredients, steps]` - `['delete_recipe', recipe_id]` Rules: - A successful add creates IDs `recipe1`, `recipe2`, ... in order of successful creation. - Recipe names must be unique case-insensitively. - `get_recipe` returns `[name, ingredients_as_string, steps_as_string]`, where each list is joined by `','`. - Preserve ingredient and step order exactly. - If a recipe does not exist, `get_recipe` returns `[]`. - Return a list containing the result of every operation, in order.

Constraints

  • 0 <= len(operations) <= 1000
  • 0 <= len(ingredients), len(steps) <= 100 per operation
  • Recipe names are unique case-insensitively among existing recipes
  • Recipe IDs in queries may refer to missing recipes

Examples

Input: [['add_recipe', 'Pasta', ['noodles', 'salt'], ['boil', 'mix']], ['get_recipe', 'recipe1'], ['update_recipe', 'recipe1', 'Cream Pasta', ['noodles', 'cream'], ['boil', 'stir']], ['get_recipe', 'recipe1'], ['delete_recipe', 'recipe1'], ['get_recipe', 'recipe1']]

Expected Output: ['recipe1', ['Pasta', 'noodles,salt', 'boil,mix'], True, ['Cream Pasta', 'noodles,cream', 'boil,stir'], True, []]

Explanation: Basic add, read, update, and delete flow.

Input: [['add_recipe', 'Soup', ['water'], ['boil']], ['add_recipe', 'soup', ['water', 'salt'], ['boil']], ['add_recipe', 'Salad', ['lettuce'], ['mix']], ['update_recipe', 'recipe2', 'SOUP', ['lettuce'], ['mix']], ['get_recipe', 'recipe2']]

Expected Output: ['recipe1', None, 'recipe2', False, ['Salad', 'lettuce', 'mix']]

Explanation: Recipe names are unique case-insensitively, both when adding and updating.

Input: [['add_recipe', 'Tea', [], []], ['get_recipe', 'recipe1'], ['delete_recipe', 'recipe2'], ['update_recipe', 'recipe3', 'X', [], []]]

Expected Output: ['recipe1', ['Tea', '', ''], False, False]

Explanation: Edge case with empty ingredient/step lists and missing recipe IDs.

Input: [['add_recipe', 'A', ['x'], ['s1']], ['delete_recipe', 'recipe1'], ['add_recipe', 'B', ['y'], ['z']], ['get_recipe', 'recipe2']]

Expected Output: ['recipe1', True, 'recipe2', ['B', 'y', 'z']]

Explanation: IDs continue increasing after deletions; they are not reused.

Hints

  1. Store recipes by `recipe_id`, and keep a second map from normalized recipe name to `recipe_id`.
  2. For uniqueness, compare names with `casefold()` rather than the original casing.

Part 2: Search and Sort Recipes

Write `solution(operations)` to simulate a recipe manager focused on searching and sorting. Each operation is one of: - `['add_recipe', name, ingredients, steps]` - `['search_recipes_by_ingredient', ingredient]` - `['list_recipes', sort_by]` Rules: - Successful adds create IDs `recipe1`, `recipe2`, ... in order of successful creation. - Recipe names must be unique case-insensitively. - Ingredient matching is exact but case-insensitive. - `search_recipes_by_ingredient` returns recipe IDs sorted by: 1. number of ingredients ascending 2. recipe ID ascending - `list_recipes(sort_by)` returns all recipe IDs sorted by: - `'name'`: recipe name ascending case-insensitively, then recipe ID ascending - `'ingredient_count'`: ingredient count ascending, then recipe ID ascending - If `sort_by` is invalid, default to `'name'`. - Return a list containing the result of every operation, in order.

Constraints

  • 0 <= len(operations) <= 1000
  • 0 <= len(ingredients), len(steps) <= 100 per recipe
  • Recipe IDs should be ordered by their numeric suffix for tie-breaking
  • If there are no recipes, search/list should return an empty list

Examples

Input: [['add_recipe', 'Pancakes', ['flour', 'eggs', 'milk'], ['mix', 'cook']], ['add_recipe', 'Omelette', ['eggs', 'butter'], ['whisk', 'cook']], ['add_recipe', 'FrenchToast', ['bread', 'eggs', 'milk'], ['dip', 'fry']], ['search_recipes_by_ingredient', 'EGGS'], ['list_recipes', 'name']]

Expected Output: ['recipe1', 'recipe2', 'recipe3', ['recipe2', 'recipe1', 'recipe3'], ['recipe3', 'recipe2', 'recipe1']]

Explanation: Search sorts by ingredient count, then by recipe ID. Listing by name is case-insensitive.

Input: [['add_recipe', 'Soup', ['water'], ['boil']], ['add_recipe', 'soup', ['water', 'salt'], ['boil']], ['add_recipe', 'Salad', ['lettuce'], ['mix']], ['list_recipes', 'unknown'], ['search_recipes_by_ingredient', 'tomato']]

Expected Output: ['recipe1', None, 'recipe2', ['recipe2', 'recipe1'], []]

Explanation: Invalid sort key defaults to name sorting, and duplicate names are rejected.

Input: [['list_recipes', 'name'], ['search_recipes_by_ingredient', 'salt']]

Expected Output: [[], []]

Explanation: Edge case: empty manager.

Input: [['add_recipe', 'A', ['x'], ['s']], ['add_recipe', 'B', ['y'], ['s']], ['add_recipe', 'C', ['z', 'q'], ['s']], ['list_recipes', 'ingredient_count'], ['search_recipes_by_ingredient', 'Q']]

Expected Output: ['recipe1', 'recipe2', 'recipe3', ['recipe1', 'recipe2', 'recipe3'], ['recipe3']]

Explanation: Tie-breaking by recipe ID matters when ingredient counts are equal.

Hints

  1. Keep the full recipe data so you can sort by either name or ingredient count later.
  2. When sorting by recipe ID, compare the numeric part after `'recipe'`, not the raw string.

Part 3: Recipe Users and Edits

Write `solution(operations)` to simulate a recipe manager with users. Each operation is one of: - `['add_recipe', name, ingredients, steps]` - `['get_recipe', recipe_id]` - `['add_user', user_id]` - `['edit_recipe', user_id, recipe_id, new_name, new_ingredients, new_steps]` Rules: - Successful recipe adds create IDs `recipe1`, `recipe2`, ... in order of successful creation. - Recipe names must remain unique case-insensitively. - Any existing user may edit any existing recipe. - `edit_recipe` fails if the user does not exist, the recipe does not exist, or the new name conflicts with another recipe. - `get_recipe` returns `[name, ingredients_as_string, steps_as_string]`, with lists joined by `','`. - Return a list containing the result of every operation, in order.

Constraints

  • 0 <= len(operations) <= 1000
  • 0 <= len(ingredients), len(steps) <= 100 per recipe
  • User IDs are case-sensitive and must be unique exactly
  • Editing a recipe to the same name with different casing is allowed if it is the same recipe

Examples

Input: [['add_recipe', 'Toast', ['bread'], ['toast']], ['add_user', 'u1'], ['edit_recipe', 'u1', 'recipe1', 'Buttered Toast', ['bread', 'butter'], ['toast', 'spread']], ['get_recipe', 'recipe1']]

Expected Output: ['recipe1', True, True, ['Buttered Toast', 'bread,butter', 'toast,spread']]

Explanation: A valid user can edit an existing recipe.

Input: [['add_recipe', 'Soup', ['water'], ['boil']], ['add_user', 'chef'], ['add_user', 'chef'], ['edit_recipe', 'ghost', 'recipe1', 'Hot Soup', ['water'], ['boil']], ['edit_recipe', 'chef', 'recipe2', 'Hot Soup', ['water'], ['boil']], ['get_recipe', 'recipe1']]

Expected Output: ['recipe1', True, False, False, False, ['Soup', 'water', 'boil']]

Explanation: Duplicate users are rejected, and missing user/recipe edits fail.

Input: [['add_recipe', 'Pie', ['fruit'], ['bake']], ['add_recipe', 'Cake', ['flour'], ['bake']], ['add_user', 'u1'], ['edit_recipe', 'u1', 'recipe2', 'PIE', ['flour'], ['bake']], ['get_recipe', 'recipe2']]

Expected Output: ['recipe1', 'recipe2', True, False, ['Cake', 'flour', 'bake']]

Explanation: Editing to a conflicting name is not allowed, even with different casing.

Input: [['add_recipe', 'Tea', [], []], ['add_user', 'u'], ['edit_recipe', 'u', 'recipe1', 'tea', [], []], ['get_recipe', 'recipe1']]

Expected Output: ['recipe1', True, True, ['tea', '', '']]

Explanation: Edge case: empty ingredients/steps and editing to the same logical name with different casing.

Hints

  1. Use one set for users and one map from normalized recipe name to recipe ID.
  2. When editing, a name is only a conflict if it belongs to a different recipe.

Part 4: Recipe Version History and Rollback

Write `solution(operations)` to simulate a recipe manager with version history. Each operation is one of: - `['add_recipe', name, ingredients, steps]` - `['get_recipe', recipe_id]` - `['update_recipe', recipe_id, name, ingredients, steps]` - `['add_user', user_id]` - `['edit_recipe', user_id, recipe_id, new_name, new_ingredients, new_steps]` - `['version_recipe', recipe_id]` - `['rollback_recipe', recipe_id, version_number]` Rules: - Successful recipe adds create IDs `recipe1`, `recipe2`, ... in order of successful creation. - Recipe names must be unique case-insensitively. - A newly added recipe has no version history yet. - Every successful `update_recipe` creates a new version whose editor is `'system'`. - Every successful `edit_recipe` creates a new version whose editor is the given `user_id`. - Versions are snapshots of the recipe state after the successful update/edit. - `version_recipe` returns all version strings in ascending order: `<version>:<name>:<ingredients_as_string>:<steps_as_string>:<last_edited_by>` - `rollback_recipe(recipe_id, version)` changes the current recipe to match that historical version and appends a brand-new version with editor `'rollback'`. - Rollback fails if the recipe does not exist, the version does not exist, or the rollback name would conflict with another current recipe. - Return a list containing the result of every operation, in order.

Constraints

  • 0 <= len(operations) <= 1000
  • 0 <= len(ingredients), len(steps) <= 100 per recipe state
  • Version numbers start at 1 for each recipe and increase by 1 per successful update/edit/rollback snapshot
  • A recipe with no successful update/edit has no version history

Examples

Input: [['add_recipe', 'Soup', ['water'], ['boil']], ['add_user', 'chef'], ['version_recipe', 'recipe1'], ['update_recipe', 'recipe1', 'Soup', ['water', 'salt'], ['boil', 'season']], ['edit_recipe', 'chef', 'recipe1', 'Tomato Soup', ['water', 'salt', 'tomato'], ['boil', 'blend']], ['version_recipe', 'recipe1'], ['rollback_recipe', 'recipe1', 1], ['get_recipe', 'recipe1'], ['version_recipe', 'recipe1']]

Expected Output: ['recipe1', True, [], True, True, ['1:Soup:water,salt:boil,season:system', '2:Tomato Soup:water,salt,tomato:boil,blend:chef'], True, ['Soup', 'water,salt', 'boil,season'], ['1:Soup:water,salt:boil,season:system', '2:Tomato Soup:water,salt,tomato:boil,blend:chef', '3:Soup:water,salt:boil,season:rollback']]

Explanation: A recipe has no history after add, gains versions after update/edit, and rollback appends a new version.

Input: [['version_recipe', 'recipe9'], ['rollback_recipe', 'recipe1', 1], ['add_recipe', 'A', [], []], ['version_recipe', 'recipe1'], ['rollback_recipe', 'recipe1', 1], ['add_user', 'u1'], ['edit_recipe', 'ghost', 'recipe1', 'B', [], []], ['update_recipe', 'recipe2', 'C', [], []]]

Expected Output: [[], False, 'recipe1', [], False, True, False, False]

Explanation: Edge cases: missing recipe, no history yet, missing user, and invalid update target.

Input: [['add_recipe', 'One', ['a'], ['s']], ['update_recipe', 'recipe1', 'PastName', ['a'], ['s1']], ['add_recipe', 'Other', ['b'], ['t']], ['update_recipe', 'recipe1', 'CurrentName', ['a'], ['s2']], ['update_recipe', 'recipe2', 'PastName', ['b'], ['t2']], ['rollback_recipe', 'recipe1', 1], ['get_recipe', 'recipe1']]

Expected Output: ['recipe1', True, 'recipe2', True, True, False, ['CurrentName', 'a', 's2']]

Explanation: Rollback can fail if the historical name now conflicts with another current recipe.

Input: [['add_recipe', 'Tea', [], []], ['update_recipe', 'recipe1', 'Herbal Tea', [], []], ['rollback_recipe', 'recipe1', 1], ['version_recipe', 'recipe1']]

Expected Output: ['recipe1', True, True, ['1:Herbal Tea:::system', '2:Herbal Tea:::rollback']]

Explanation: Rolling back to the same current state is still valid and creates a new version entry.

Hints

  1. Store immutable snapshots for each recipe history; do not overwrite old versions.
  2. When rolling back, apply the historical state to the current recipe, then append a new history entry labeled `'rollback'`.
Last updated: Apr 19, 2026

Loading coding console...

PracHub

Master your tech interviews with 8,500+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • Find User Airport at a Time - Ramp (medium)
  • Find an Exit in a URL Maze - Ramp (medium)
  • Build a Wordle-style game in React - Ramp (medium)
  • Find final URL by crawling until “congrats” - Ramp (hard)
  • Implement multi-level task manager APIs - Ramp (medium)