PracHub
QuestionsCoachesLearningGuidesInterview Prep

Quick Overview

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.

  • medium
  • Coinbase
  • Coding & Algorithms
  • Software Engineer

Implement a versioned recipe management system

Company: Coinbase

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Take-home Project

You are asked to implement an in-memory **Recipe Management System** (similar in difficulty to a simple banking/in-memory DB exercise). The system supports incremental feature “levels”. Design the data structures and implement the required APIs. ## Data model Assume each **Recipe** has at least: - `recipeId` (unique string or integer) - `name` (string) - `size` (integer; used only for sorting) Assume each **User** has at least: - `userId` (unique) - `userName` No persistence is required (everything is in memory). --- ## Level 1: Basic CRUD for recipes Implement the following operations: - `addRecipe(recipe)`: add a new recipe (reject or overwrite if `recipeId` already exists—state your choice clearly). - `updateRecipe(recipeId, fields...)`: update an existing recipe. - `getRecipe(recipeId)`: return the recipe or “not found”. - `deleteRecipe(recipeId)`: delete an existing recipe. Define expected behavior for edge cases (e.g., updating/deleting a non-existent recipe). --- ## Level 2: Search and list Implement: - `searchRecipes(query)`: search recipes by **name**, **case-insensitive** (define whether it’s exact match or substring match; choose one and be consistent). - `listRecipes(sortBy)`: return all recipes sorted by either: - `name` (lexicographic, case-insensitive), or - `size` (numeric ascending) Specify tie-breaker behavior (e.g., tie-break by `recipeId`). --- ## Level 3: Add user Implement: - `addUser(user)`: add a user (keep it simple—no extra features unless needed). Define behavior on duplicate `userId`. --- ## Level 4: Version history and rollback Add **versioning** to support: - `versionHistory()`: return stored version metadata (at minimum, a monotonic `versionId` and what changed). - `rollback(versionId)`: restore the system state to exactly how it was at that `versionId`. Guidance: treat each mutating operation (e.g., add/update/delete recipe, add user) as producing a new version; maintain whatever additional structures you need (e.g., a `versionMap`). --- ## Constraints / expectations - Assume up to ~100k recipes/users. - Aim for clean APIs and reasonable time complexity. - You may implement this as a class/module with methods in your preferred language.

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

Implement an in-memory recipe store that supports adding, updating, retrieving, and deleting recipes. Each recipe has recipeId, name, and size. This problem uses a batch API: process the operations in order and return one result for each operation. Duplicate addRecipe calls are rejected and return False. Updating or deleting a missing recipe returns False. getRecipe for a missing recipe returns None.

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

  1. Use a hash map keyed by recipeId so that CRUD operations are average O(1).
  2. When returning a recipe, return a copy rather than the internal object to avoid accidental mutation.

Part 2: Level 2 - Search and List Recipes

Extend the in-memory recipe store from Level 1 with search and sorted listing. The CRUD behavior is the same as in Part 1: duplicate addRecipe is rejected, missing updateRecipe and deleteRecipe return False, and missing getRecipe returns None. searchRecipes performs a case-insensitive substring search on recipe names. listRecipes returns all recipes sorted either by case-insensitive name or by numeric size. Ties are broken by recipeId ascending.

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

  1. Normalize names with lower() for both searching and name sorting.
  2. 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

Extend the Level 2 recipe system with basic user creation. The recipe CRUD, search, and list behavior is unchanged. Add a separate user store supporting addUser. Duplicate userId values are rejected and return False. User IDs and recipe IDs are separate namespaces, so a recipeId may equal a userId without conflict.

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

  1. Keep users and recipes in separate dictionaries.
  2. Do not let addUser affect recipe search, list, get, update, or delete behavior.

Part 4: Level 4 - Version History and Rollback

Extend the Level 3 system with versioning. The initial empty state is version 0 with change initial. Each successful addRecipe, updateRecipe, deleteRecipe, addUser, and valid rollback creates a new monotonic version. Failed operations, such as duplicate adds or deleting a missing recipe, do not create versions. versionHistory returns metadata for all created versions. rollback(versionId) restores recipes and users to the exact data state represented by that versionId, then records the rollback itself as a new version whose data state equals the target version.

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

  1. Represent each version as a node with a parent pointer and the single delta that created it.
  2. To rollback, undo deltas from the current version up to the lowest common ancestor with the target, then apply deltas down to the target.
Last updated: Jul 7, 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
  • AI Coding 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

  • Implement an In-Memory Database - Coinbase (hard)
  • Implement a Coin-Constrained Jump Strategy - Coinbase (hard)
  • Implement Game Physics and Block Mining - Coinbase (hard)
  • Compute Total Manual Distance - Coinbase (medium)
  • Implement a Flappy Bird Jump Agent - Coinbase (medium)