Food cost is the most operationally critical metric for any restaurant business. Most restaurant SaaS products handle inventory and sales but leave the connection between them to manual calculation. Building the recipe costing layer that automatically tracks food cost percentage from ingredient purchases through menu item sales is the engineering problem this article addresses.
Food cost is the most operationally critical metric for any restaurant business. Most restaurant SaaS products handle inventory and sales separately but leave the connection between them to manual calculation by the chef or manager. Building the recipe costing layer that automatically derives food cost percentage from ingredient purchases through menu item sales is one of the most valuable features a restaurant SaaS can offer, and one of the more complex engineering problems to get right.
The data model: ingredients, recipes, and menu items
The core of the system is a three-level hierarchy:
Ingredient level. An ingredient is a purchasable item with a unit of measure, a current cost per unit, and a category (meat, produce, dairy, dry goods). The cost per unit is updated when purchase orders are recorded, using a weighted average cost calculation across batches.
Recipe level. A recipe is a composition of ingredients with specific quantities and units. A recipe may also include sub-recipes (a sauce that is used as an ingredient in multiple dishes). The recipe has a calculated cost based on the sum of its ingredient quantities times their current unit costs.
Menu item level. A menu item maps to one or more recipes with a defined yield. The menu item has a selling price, and the system calculates the food cost percentage as (recipe cost / selling price) * 100.
CREATE TABLE ingredients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
name TEXT NOT NULL,
unit TEXT NOT NULL, -- kg, liter, piece, etc.
cost_per_unit NUMERIC(12,4) NOT NULL DEFAULT 0,
category TEXT NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE recipes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
name TEXT NOT NULL,
yield_quantity NUMERIC(10,3) NOT NULL DEFAULT 1,
yield_unit TEXT NOT NULL
);
CREATE TABLE recipe_ingredients (
recipe_id UUID REFERENCES recipes(id),
ingredient_id UUID REFERENCES ingredients(id),
quantity NUMERIC(10,4) NOT NULL,
unit TEXT NOT NULL,
PRIMARY KEY (recipe_id, ingredient_id)
);
CREATE TABLE menu_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
name TEXT NOT NULL,
selling_price NUMERIC(10,2) NOT NULL,
recipe_id UUID REFERENCES recipes(id)
);
Calculating recipe cost in Go
Recipe cost calculation is a recursive computation when sub-recipes are involved. A sub-recipe is a recipe that is used as an ingredient in another recipe. The cost of the parent recipe depends on the cost of its sub-recipes, which themselves depend on their own ingredients.
The recursion terminates at leaf-level ingredients that have a direct cost per unit. In Go, a recursive cost calculation for a recipe:
type CostCalculator struct {
repo RecipeRepository
// memo cache avoids recomputing costs for shared sub-recipes
memo map[uuid.UUID]decimal.Decimal
}
func (c *CostCalculator) RecipeCost(
ctx context.Context,
recipeID uuid.UUID,
) (decimal.Decimal, error) {
if cost, ok := c.memo[recipeID]; ok {
return cost, nil
}
components, err := c.repo.GetRecipeComponents(ctx, recipeID)
if err != nil {
return decimal.Zero, err
}
total := decimal.Zero
for _, comp := range components {
if comp.IsSubRecipe {
subCost, err := c.RecipeCost(ctx, comp.SubRecipeID)
if err != nil {
return decimal.Zero, err
}
// scale by the quantity used of the sub-recipe
total = total.Add(subCost.Mul(comp.Quantity))
} else {
// leaf ingredient
ingredientCost := comp.CostPerUnit.Mul(comp.Quantity)
total = total.Add(ingredientCost)
}
}
c.memo[recipeID] = total
return total, nil
}
The memo cache is important when the same sub-recipe appears in multiple parent recipes. Without it, the cost calculation for a stock or sauce base that is shared across dozens of dishes would be recalculated for every dish in a menu update.
Weighted average cost for ingredient pricing
Ingredient costs change with each purchase. A kilogram of tomatoes purchased in winter costs differently from summer. The industry standard for tracking ingredient cost over time is the weighted average cost (WAC) method.
When a new purchase order is recorded, the WAC for each ingredient is recalculated:
new WAC = (existing stock quantity * existing WAC + new quantity * new unit cost)
/ (existing stock quantity + new quantity)
In PostgreSQL, this update runs atomically within a transaction that records the purchase order and updates the ingredient cost simultaneously. This ensures that the cost displayed in recipe calculations always reflects the current weighted average, not a stale price.
For Lebanese restaurants dealing with LBP/USD dual-currency pricing (a common operational reality in Lebanon), store the purchase cost in the currency of the invoice and a USD equivalent at the transaction's exchange rate. Report food costs in both currencies. When the LBP/USD rate shifts, historical food costs remain accurate because they were recorded at the exchange rate at purchase time.
Automatic food cost percentage tracking against sales
When a point-of-sale order is recorded, the system should automatically derive the cost of goods sold (COGS) for that order by multiplying each menu item quantity by its recipe cost.
This creates an event-driven flow: a new order record triggers an update to the food cost tracking table for that day. The aggregation gives the kitchen manager a daily food cost percentage without requiring manual calculation:
CREATE TABLE daily_food_cost (
tenant_id UUID NOT NULL,
cost_date DATE NOT NULL,
total_revenue NUMERIC(12,2) DEFAULT 0,
total_cogs NUMERIC(12,2) DEFAULT 0,
food_cost_pct NUMERIC(5,2) GENERATED ALWAYS AS
(CASE WHEN total_revenue > 0
THEN (total_cogs / total_revenue * 100)
ELSE 0 END) STORED,
PRIMARY KEY (tenant_id, cost_date)
);
The food_cost_pct column uses a generated column so the percentage is always computed from the actual revenue and COGS values, never stored as a manually set figure.
Waste and spoilage tracking
Waste is the gap between theoretical and actual food cost. Theoretical food cost is what the system calculates based on recipes and sales. Actual food cost is what was actually purchased minus what remains in inventory.
The system tracks waste through two mechanisms. Staff record waste entries when ingredients are discarded (spoiled produce, dropped dishes, trimming waste). The POS system records voids and comp items, which represent revenue lost but not associated with inventory consumption.
Comparing theoretical food cost (derived from recipe costs times sales) against actual food cost (derived from purchases minus ending inventory) gives the kitchen manager visibility into where waste is occurring and at what scale. A restaurant running a theoretical food cost of 28% but an actual food cost of 35% has 7 percentage points of unaccounted waste. For a restaurant with $100,000 monthly revenue, that is $7,000 per month in preventable waste.
Key lessons from production
Use shopspring/decimal for all cost calculations. Floating-point arithmetic produces wrong results for food cost percentages. A recipe that costs $4.8752 displayed as $4.87 but calculated as $4.876 will produce incorrect food cost percentages over thousands of orders.
Build the memo cache for recursive recipe cost calculations from the start. Restaurant menus have shared sub-recipes everywhere. Without memoization, a full menu cost recalculation on a 100-item menu with shared sauces and bases takes seconds instead of milliseconds.
Store ingredient costs with WAC update history. The ability to see what an ingredient cost on a specific past date is essential for investigating anomalous food cost percentages from previous periods.
Track the exchange rate at purchase time for multi-currency operations. Lebanese restaurants in particular need food costs that remain meaningful as the LBP/USD rate changes.
Enjoying this article?
Enter your email and get a clean, formatted PDF of this article - free, no spam.
Not sure where to start?
Voxire builds restaurant SaaS systems including POS, inventory, and food cost tracking for operators across Lebanon and the MENA region. If you are building or extending a restaurant management platform, we can help with the engineering architecture.
https://voxire.com/get-a-quote/


