Implement a Multi-Level Recipe Management Service
Company: Airbnb
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Take-home Project
## Implement a Multi-Level Recipe Management Service
Design and implement an in-memory recipe manager. A recipe stores a name, an ordered ingredient list, and an ordered step list. Complete the following three levels while preserving the behavior from earlier levels.
For this practice contract, recipe names and ingredient comparisons use one documented case-insensitive normalization. Recipe IDs are `recipe1`, `recipe2`, and so on; the numeric sequence advances only after a successful creation and deleted IDs are never reused. “Recipe ID order” means ascending numeric suffix.
### Part 1 — Create, Read, Update, and Delete Recipes
Implement these operations:
- `addRecipe(name, ingredients, steps) -> recipeId | null`: create a recipe unless another current recipe has the same normalized name.
- `getRecipe(recipeId) -> values`: return `[name, ingredientsJoined, stepsJoined]`, where each list is joined with commas in its stored order; return an empty list when the ID does not exist.
- `updateRecipe(recipeId, name, ingredients, steps) -> updated`: replace an existing recipe unless the normalized name belongs to a different recipe.
- `deleteRecipe(recipeId) -> deleted`: delete an existing recipe and return whether anything was removed.
#### What This Part Should Cover
- A primary map by recipe ID and a consistent uniqueness index by normalized name.
- Input-list copies so callers cannot mutate stored recipes indirectly.
- Conflict checks that allow a recipe to retain its own name.
- Correct cleanup of every index on update and deletion.
```hint Treat the name index as state, not a search shortcut
Every successful create, rename, or delete must leave the primary records and normalized-name ownership in agreement.
```
### Part 2 — Search and Sort Recipes
Implement:
- `searchRecipesByIngredient(ingredient) -> recipeIds`: return recipes containing a case-insensitive ingredient match, sorted by ingredient-list length and then recipe ID.
- `listRecipes(sortBy) -> recipeIds`: for `ingredient_count`, sort by ingredient-list length and then recipe ID; for `name`, sort by normalized name and then recipe ID. An unsupported `sortBy` value defaults to `name`.
Ingredient-list length counts stored entries. Search tests whether at least one stored entry matches; it does not return a recipe more than once.
#### What This Part Should Cover
- Case-insensitive membership under the same normalization contract.
- Deterministic secondary ordering by the recipe ID's numeric suffix.
- Preservation of ingredient and step order in the stored recipe.
- Either a correct scan or indexes that are updated atomically with recipes.
```hint Separate filtering from ordering
First identify each matching recipe once, then apply the complete two-key comparator to the results.
```
### Part 3 — Add Users and User-Initiated Edits
Implement:
- `addUser(userId) -> added`: add an exact, case-sensitive user ID unless it already exists.
- `editRecipe(userId, recipeId, newName, newIngredients, newSteps) -> edited`: any registered user may edit any existing recipe. Return `false` for a missing user, missing recipe, or normalized-name conflict with another recipe; otherwise apply the update and return `true`.
#### What This Part Should Cover
- A user registry independent of recipe ownership.
- Reuse of the same recipe-update validation and index maintenance.
- No partial mutation when any precondition fails.
- Clear behavior when the recipe keeps its current normalized name.
```hint Use one update path
The direct update and user-initiated edit should not implement name-conflict and index rules differently.
```
### Part 4 — Preserve Invariants Across Levels
Explain the invariants, complexity, and tests needed for mixed operation sequences.
#### What This Part Should Cover
- One live record per recipe ID and one owner per normalized recipe name.
- Monotonic, non-reused IDs even after deletion.
- Immutable snapshots or defensive copies at the API boundary.
- Tests that interleave rename, delete, search, sort, user edits, and failed operations.
```hint Test transitions, not isolated methods
A rename followed by reuse of the old name and deletion of the new owner exposes index bugs that single-operation tests miss.
```
### What a Strong Answer Covers
- Exact return values and deterministic ordering for every operation.
- One normalization policy used consistently for uniqueness and ingredient search.
- Atomic maintenance of recipe records and secondary indexes.
- Reused validation logic, defensive copying, and stateful regression tests across all three levels.
### Follow-up Questions
1. Which operations change if recipe names must be versioned rather than updated in place?
2. How would you add an ingredient index without making rename and delete inconsistent?
3. What should happen if two requests concurrently try to claim the same normalized name?
4. How would you serialize and restore the service while preserving the next recipe ID?
Quick Answer: Implement an in-memory recipe service that grows from basic CRUD into deterministic search, sorting, and user-initiated edits. The task evaluates normalization, secondary-index consistency, defensive copying, monotonic identifiers, atomic validation, and mixed-operation testing.