Katkat

Implement a Multi-Level Recipe Management Service

AirbnbSoftware Engineer · Online Assessment

Last updated

Design and implement an in-memory recipe management service supporting deterministic querying, defensive data isolation, atomic updates, and secondary index maintenance.

Problem Description

Design an in-memory recipe management service, RecipeService, that supports normalized record storage, atomic updates, and deterministic search queries.

Requirements

  1. Data Normalization & Monotonic IDs:

    • Each created recipe is assigned an integer ID starting from 1, incrementing by 1 for each subsequent recipe.
    • Ingredients and tags must be normalized: stripped of leading/trailing whitespace and converted to lowercase. Duplicate entries within a recipe must be deduplicated while preserving their first occurrence's order.
    • Input and output data collections must be defensively copied so that external mutations do not affect internal state.
  2. Atomic Updates:

    • Updates to a recipe must be atomic. If any validation fails (e.g., negative prep_time or nonexistent recipe_id), the update must fail entirely without modifying the recipe or secondary indexes.
  3. Deterministic Search:

    • Filter recipes by an optional tag (case-insensitive) and an optional max_prep_time (prep_time <= max_prep_time).
    • Sort results by sort_by:
      • "prep_time_asc": sort by prep_time ascending, tie-breaking by id ascending.
      • "prep_time_desc": sort by prep_time descending, tie-breaking by id ascending.
      • "title_asc": sort by title lexicographically ascending, tie-breaking by id ascending.
    • Returns a list of matching recipe IDs.

Interface

class RecipeService:
    def __init__(self):
        pass

    def create_recipe(self, title: str, ingredients: list[str], tags: list[str], prep_time: int) -> int:
        """Creates a recipe and returns its assigned ID."""
        pass

    def get_recipe(self, recipe_id: int) -> dict | None:
        """Returns a dictionary with keys: 'id', 'title', 'ingredients', 'tags', 'prep_time'. Returns None if not found."""
        pass

    def update_recipe(self, recipe_id: int, updates: dict) -> bool:
        """Updates allowed fields ('title', 'ingredients', 'tags', 'prep_time'). Returns True if successful, False otherwise."""
        pass

    def delete_recipe(self, recipe_id: int) -> bool:
        """Deletes a recipe by ID. Returns True if deleted, False if not found."""
        pass

    def search_recipes(self, tag: str | None = None, max_prep_time: int | None = None, sort_by: str = "prep_time_asc") -> list[int]:
        """Returns matching recipe IDs ordered according to sort_by."""
        pass

Examples

service = RecipeService()
id1 = service.create_recipe("Pancakes", ["Flour", " Milk ", "flour"], ["Breakfast", " quick "], 20)
# id1 == 1
# Ingredients stored: ["flour", "milk"]
# Tags stored: ["breakfast", "quick"]

id2 = service.create_recipe("Waffles", ["flour", "eggs"], ["breakfast"], 20)
# id2 == 2

service.search_recipes(tag="breakfast", max_prep_time=25, sort_by="prep_time_asc")
# Returns: [1, 2] (both have prep_time 20, broken by ID ascending)

service.update_recipe(1, {"prep_time": -5})
# Returns: False (invalid prep_time; state is unchanged)

service.delete_recipe(1)
# Returns: True

service.search_recipes(tag="breakfast")
# Returns: [2]

Constraints

  • prep_time >= 0 for all valid records.
  • title is non-empty.
  • Total operations <= 2 * 10^4.
  • Number of recipes <= 5,000.

Hints

Hint 1

Normalize and deduplicate tags and ingredients in a helper function used by both creation and update operations.

Hint 2

Validate all update keys and values upfront before modifying any internal record or inverted index to ensure atomicity.

Hint 3

Ensure defensive copies are created both when ingesting lists and when returning records from `get_recipe`.

Follow-up questions

  • How would you optimize `search_recipes` if the dataset grew to millions of records with high write concurrency?
  • How would you extend this service to support pagination with stable cursors across concurrent edits?
  • What changes would be necessary to support multi-tag boolean filtering (AND / OR / NOT) efficiently?