Back

Recipe Manager (Online Assessment)

Low-Level DesignCodingSoftware EngineerReported Apr, 2026

Overview

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

Level 1: Basic CRUD Operations

Description

Initially, the recipe manager does not contain any recipes. Implement operations to allow recipe creation, retrieval, updates, and deletion.

Operations

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

Level 2: Search and Sorting

Description

The recipe manager should support searching for recipes and returning results in a specific sorted order.

New Operations

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

Level 3: User System

Description

The recipe manager should now incorporate a user system. Certain operations require user validation before they can be performed.

New Operations

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.

Level 4: Version Control

Description

The recipe manager should maintain a version history for each recipe. Updates no longer overwrite existing data but instead create new versions.

Modified Behavior

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

New Operations

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", [...])

Important Edge Cases

Case-Insensitive Name Handling

"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

Rollback Name Conflicts

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

Version Numbering

Versions are numbered starting from 1

Each edit (including rollback) increments the version number

Version numbers are never reused or reset

Constraints

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

Sample Solution

⚠️ 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

Example Usage

manager = RecipeManager()

Level 1: Basic CRUD

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)

Level 2: Search and Sort

results = manager.search_recipes("salad") # Returns Caesar Salad

Level 3: User System

manager.add_user("u1", "chef_john")

manager.edit_recipe("u1", "1", "Classic Bolognese", ["pasta", "beef", "tomatoes", "herbs"])

Level 4: Version Control

history = manager.get_recipe_history("1") # Shows all versions

manager.rollback_recipe("u1", "1", 1) # Rollback to original version


Auto-save enabled
Loading editor…
Output
Run your code to see the output here.