Week two of building MiseOS, and I still haven’t written a single line of Java.
Good.
Because before you can build software, you need to understand the real world you’re trying to model. Not the idealized version. Not what you think happens. What actually happens in a professional kitchen when they plan a weekly menu.
The Danger of Jumping Straight to Code#
My first instinct was to start coding immediately. Open IntelliJ, create a Menu class, add some fields, and see what happens.
public class Menu {
private Long id;
private int weekNumber;
private int year;
private List<Dish> dishes;
//etc.
}I’ve done this before on smaller projects. It always ends the same way: three days in, I realize the model is wrong. The relationships don’t make sense. The workflow I coded doesn’t match reality. And now I have to refactor everything.
In my former life as a chef, we had a saying: “A good plan at the start of the day saves chaos at service.”
The same principle applies to software. If you don’t understand the domain—the actual business problem—you’ll build the wrong thing. So I spent this week doing something that felt unproductive but was actually the most important work: mapping the real world.
Mapping the Real World#
I called up former colleagues from my 20 years in the kitchen industry. I wanted to understand the details of the friction points in menu planning.
The Messy Reality#
The chaos isn’t because cooks are disorganized; it’s because the creative process (suggesting dishes) and the operational process (ordering ingredients) are disconnected. Notes get lost, verbal requests are forgotten, and the Head Chef ends up guessing the grocery list on Friday afternoon.
The Domain Model (The “Map”)#
After these conversations, I sketched out the “MiseOS Domain Model.” This isn’t a database schema yet; it’s a map of entities and their responsibilities.
Key takeaways from the model:
- Assigned Roles: Line cooks belong to specific Stations (Hot, Cold, Pastry).
- The Bridge: The Menu Proposal is the bridge between a cook’s idea and the final menu.
- Operational Fuel: Ingredient Requests can be “Required for” a specific dish or submitted as general stock needs.
- Aggregation: The system must consolidate these requests into a single Grocery List with a specific Delivery Day.
- Editor (Head Chef): Reviews, edits, and approves Menu Proposals and Ingredient Requests, then publishes the final Menu.
From Domain to User Stories#
With the domain mapped, I could write user stories that define the MVP.
Here are a few key examples:
Epic 1: Menu Suggestions#
As a Line Cook, I want to suggest dishes for next week so my ideas aren’t forgotten.
- Criteria: Submit name, description, and allergens. Suggestions are tied to my assigned station.
As a Head Chef, I want to review pending suggestions in one place so I can curate the menu.
- Criteria: Approve, reject, or edit suggestions.
Epic 2: Ingredient Management#
As a Line Cook, I want to request ingredients (either for a dish or general stock) so I have what I need for service.
- Criteria: Specify quantity, unit, and notes (e.g., “Check dry storage first”).
As a Head Chef, I want a consolidated Grocery List so I can order efficiently, without checking five different stations for their needs.
- Criteria: Aggregate identical items (e.g., 3x 5kg onions = 15kg total). Export as a clean list.
Translating to Technical Model#
User stories reveal requirements. Requirements shape the database design.
Here’s how the domain concepts map to entities and relationships:
classDiagram
%% User relationships
User "1" --> "0..1" Station : is assigned to
User "1" --> "*" DishSuggestion : creates
User "1" --> "*" DishSuggestion : reviews
User "1" --> "*" IngredientRequest : creates
User "1" --> "*" IngredientRequest : reviews
User "1" --> "*" ShoppingList : creates
User "1" --> "*" WeeklyMenu : publishes
%% Station relationships
Station "1" --> "*" DishSuggestion : has suggestions from
Station "1" --> "*" WeeklyMenuSlot : appears in
%% DishSuggestion relationships
DishSuggestion "*" --> "*" Allergen : contains
DishSuggestion "1" --> "*" IngredientRequest : requires
DishSuggestion "0..1" --> "0..1" WeeklyMenuSlot : appears in
%% WeeklyMenu relationships
WeeklyMenu "1" --> "25" WeeklyMenuSlot : contains
%% ShoppingList relationships
ShoppingList "1" --> "*" ShoppingListItem : contains
ShoppingListItem "*" ..> "*" IngredientRequest : aggregates
Key technical decisions visible in this model:
Many-to-Many: DishSuggestion ↔ Allergen requires a junction table. A dish can have multiple allergens (gluten, dairy), and an allergen appears in many dishes.
Optional Relationships:
User → Stationis0..1because the Head Chef might not be assigned to a specific stationDishSuggestion → WeeklyMenuSlotis0..1because not every suggestion gets published
Aggregation Pattern: ShoppingListItem doesn’t directly own IngredientRequest entities—it aggregates them. Multiple requests for “onions” become one line item with a summed quantity.
Fixed Cardinality: WeeklyMenu always has exactly 25 slots (5 days × 5 stations). Empty slots are represented with is_empty = true.
This technical model will become the foundation for the ERD and JPA entities.
Revised Lessons Learned (The “Aha!” Moments)#
If I had jumped straight into coding, I would have missed several critical business rules that make a kitchen actually function:
Deadlines and Time-Locks: In the kitchen, “time is an ingredient.” I realized that suggestions cannot be an open-ended process. I’ve implemented a Wednesday 12:00 PM deadline. After this point, the system locks suggestions for the upcoming week and shifts the focus to the week after. This prevents “last-minute chaos” for the Head Chef and ensures orders are placed on time.
Historical Inspiration: A great dish shouldn’t be deleted just because the week is over. I realized I need to store Historical Suggestions. This allows a cook or chef to browse through a library of previously approved (or even rejected) ideas to find inspiration for future menus, rather than starting from a blank Word doc every Monday.
Hybrid Ingredient Requests: Initially, I thought every ingredient had to be tied to a specific dish. But kitchens also need “the basics”—oil, salt, or milk for the staff’s coffee. My model now supports Hybrid Requests:
Dish-specific: “I need 2kg of Saffron for the Paella.”
General Stock: “We are low on frying oil—add two crates to the list.”
This ensures the shopping list is 100% accurate, capturing both recipe components and general inventory needs.
Empty Slots are Intentional: A blank space on a menu grid isn’t a missing piece of data—it’s a decision. Whether it’s “Leftover Wednesday” or a “Kitchen Cleaning Day,” the system must treat an empty slot as a valid state, not an error.
What’s Next?#
The domain is mapped. The requirements are clear. Time to design the actual database.
Next post: “Designing the Database: From Domain Model to ERD”
Some decisions I need to make:
- Can a Line Cook write “løg”, “onions”, or “Onion” — or do I force a dropdown?
- When a menu is published, do dish suggestions disappear or stick around for inspiration?
- How do I represent “Leftover Wednesday” in a database that expects dishes?
That’s next task: designing the schema and translating it into JPA entities with Hibernate. But for now, I’m glad I spent the time understanding the kitchen before trying to automate it
This is part 2 of my MiseOS development log. Follow along as I build a tool for professional kitchens, one commit at a time.
