- Production BC: Aggregates (Recipe, Batch, ProductionOrder) mit Invarianten, Drafts, Status-Maschinen, Domain Events und Chargen-Genealogie - Quality BC: 9 Aggregates (TemperatureLog, CleaningPlan/Record, GoodsReceiptInspection, SampleRecord, TrainingRecord, MaintenanceRecord, QualityHold, ProcessParameter) mit HACCP-Compliance - Inventory BC: 4 Aggregates (Stock, StockMovement, InventoryCount, StorageLocation) mit FEFO, Reservierungen mit Priorität, Vier-Augen-Prinzip bei Inventur - Ubiquitous Language: Inventory-Sektion von 11 auf 27 Begriffe erweitert - Alte deutsche Datei 05-qualitaets-kontext.md entfernt (ersetzt durch 05-quality-bc.md)
35 KiB
Production BC - Detailliertes Domain Model
Bounded Context: Production Domain Type: CORE Verantwortung: Rezepturverwaltung, Produktionsplanung, Chargen-Erzeugung mit lückenloser Rückverfolgbarkeit
Ubiquitous Language
| Begriff (DE) | Begriff (EN/Code) | Typ | Definition |
|---|---|---|---|
| Rezept | Recipe | Aggregate | Mehrstufige Anleitung zur Herstellung eines Produkts aus Rohstoffen und Zwischenprodukten |
| Zutat | Ingredient | Entity | Rohstoff oder Zwischenprodukt in einem Rezept. Position bestimmt die Reihenfolge |
| Produktionsschritt | ProductionStep | Entity | Einzelner Arbeitsschritt mit optionaler Dauer und Temperaturangabe |
| Rezepttyp | RecipeType | VO (Enum) | RAW_MATERIAL, INTERMEDIATE oder FINISHED_PRODUCT |
| Ausbeute | YieldPercentage | VO | Input→Output in % (1-200%). Bei 80% braucht man 125kg für 100kg Output |
| Haltbarkeitsdauer | ShelfLifeDays | VO | Tage ab Produktionsdatum bis MHD. Definiert im Rezept |
| Unter-Rezept | SubRecipe | Concept | Verweis auf INTERMEDIATE-Rezept als Zutat (verschachtelte Rezepturen) |
| Rezeptur-Version | Version | VO | Neue Version = neues Recipe-Objekt, altes wird archiviert |
| Charge | Batch | Aggregate | Eindeutig identifizierte Produktionseinheit mit Genealogie |
| Chargennummer | BatchNumber | VO | Formatierte Kennung (Format: "P-YYYY-MM-DD-XXX") |
| Verbrauch | Consumption | Entity | Dokumentation einer verwendeten Input-Charge. Bildet die Genealogie |
| Chargen-Genealogie | Batch Genealogy | Concept | Gesamtheit aller Consumption-Beziehungen. Vorwärts-/Rückwärts-Tracing |
| Vorwärts-Tracing | traceForward | Concept | Rohstoff-Charge → betroffene Endprodukt-Chargen (Rückruf) |
| Rückwärts-Tracing | traceBackward | Concept | Endprodukt-Charge → verwendete Rohstoff-Chargen |
| Ausschuss | Waste | VO | Verlustmenge bei Produktion (Schwund, Knochen, Fett-Trimmen) |
| Produktionsauftrag | ProductionOrder | Aggregate | Geplante Produktion mit Rezept, Menge, Termin, Priorität |
| Priorität | Priority | VO (Enum) | LOW, NORMAL, HIGH, URGENT |
| Freigabe | Release | Concept | PLANNED → RELEASED. Material verfügbar, Produktion darf starten |
| Catch-Weight | Dual Quantity | Concept | Doppelte Mengenführung: Stück + Gewicht (z.B. "10 Stk à 2,3 kg") |
| MHD | BestBeforeDate | VO | Mindesthaltbarkeitsdatum = ProductionDate + ShelfLifeDays |
| Materialbedarf | Material Requirement | Concept | Berechnete Rohstoffmenge aus Rezeptur inkl. Ausbeute |
Aggregate-Übersicht
---
config:
theme: neutral
look: classic
layout: elk
themeVariables:
background: "#f8fafc"
class:
hideEmptyMembersBox: true
---
classDiagram
class Recipe {
+RecipeId id
+ArticleId articleId
+String name
+int version
+RecipeType recipeType
+YieldPercentage yieldPercentage
+int shelfLifeDays
+RecipeStatus status
+List~Ingredient~ ingredients
+List~ProductionStep~ productionSteps
+create(RecipeDraft) Result~RecipeError, Recipe~
+addIngredient(IngredientDraft) Result~RecipeError, Void~
+removeIngredient(IngredientId) Result~RecipeError, Void~
+addProductionStep(ProductionStepDraft) Result~RecipeError, Void~
+activate() Result~RecipeError, Void~
+archive() Result~RecipeError, Void~
+calculateRequiredInput(Quantity) Quantity
+calculateBestBeforeDate(LocalDate) LocalDate
}
class Ingredient {
+IngredientId id
+int position
+ArticleId articleId
+Quantity quantity
+RecipeId subRecipeId
+boolean substitutable
}
class ProductionStep {
+int stepNumber
+String description
+Integer durationMinutes
+Integer temperatureCelsius
}
class Batch {
+BatchId id
+BatchNumber batchNumber
+RecipeId recipeId
+ArticleId articleId
+Quantity plannedQuantity
+Quantity actualQuantity
+Quantity waste
+LocalDate productionDate
+LocalDate bestBeforeDate
+BatchStatus status
+UserId producedBy
+BranchId branchId
+List~Consumption~ consumptions
+plan(BatchDraft) Result~BatchError, Batch~
+startProduction() Result~BatchError, Void~
+recordConsumption(ConsumptionDraft) Result~BatchError, Void~
+complete(Quantity, Quantity, String) Result~BatchError, Void~
+cancel(String) Result~BatchError, Void~
+isFullyTraceable() boolean
+getUpstreamBatchIds() List~BatchId~
}
class Consumption {
+ConsumptionId id
+BatchId inputBatchId
+ArticleId articleId
+Quantity quantityUsed
+Instant consumedAt
}
class ProductionOrder {
+ProductionOrderId id
+RecipeId recipeId
+ArticleId articleId
+Quantity plannedQuantity
+LocalDate plannedDate
+Priority priority
+ProductionOrderStatus status
+UserId createdBy
+Instant createdAt
+BranchId branchId
+BatchId batchId
+create(ProductionOrderDraft) Result~ProductionOrderError, ProductionOrder~
+release() Result~ProductionOrderError, Void~
+startProduction(BatchId) Result~ProductionOrderError, Void~
+complete() Result~ProductionOrderError, Void~
+cancel(String) Result~ProductionOrderError, Void~
+reschedule(LocalDate) Result~ProductionOrderError, Void~
}
Recipe "1" *-- "*" Ingredient : enthält
Recipe "1" *-- "*" ProductionStep : enthält
Batch "1" *-- "*" Consumption : enthält
ProductionOrder "1" --> "0..1" Batch : erzeugt
Recipe ..> RecipeId
Recipe ..> ArticleId
Recipe ..> YieldPercentage
Recipe ..> RecipeType
Recipe ..> RecipeStatus
Batch ..> BatchId
Batch ..> BatchNumber
Batch ..> BatchStatus
Batch ..> Quantity
ProductionOrder ..> ProductionOrderId
ProductionOrder ..> Priority
ProductionOrder ..> ProductionOrderStatus
Aggregates
1. Recipe (Aggregate Root)
Verantwortung: Verwaltet mehrstufige Rezepturen mit Zutaten, Produktionsschritten und Ausbeute-Berechnung.
Recipe (Aggregate Root)
├── RecipeId (VO)
├── ArticleId (VO) - Referenz auf Stammdaten-Artikel (Endprodukt)
├── Name (VO)
├── Version (VO: int) - Versionierung bei Rezepturänderungen
├── RecipeType (VO: RAW_MATERIAL | INTERMEDIATE | FINISHED_PRODUCT)
├── YieldPercentage (VO) - Ausbeute in % (1-200)
├── ShelfLifeDays (VO: int) - Haltbarkeitsdauer → MHD-Berechnung
├── Ingredients[] (Entity)
│ ├── IngredientId (VO)
│ ├── Position (VO: int) - Reihenfolge im Rezept
│ ├── ArticleId (VO) - Referenz auf Stammdaten-Artikel (Zutat)
│ ├── Quantity (VO) - Menge mit UnitOfMeasure + optionalem Catch-Weight
│ ├── SubRecipeId (VO) - Für Zwischenprodukte (verschachtelte Rezepte)
│ └── Substitutable (boolean) - Kann durch Alternativzutat ersetzt werden?
├── ProductionSteps[] (Entity)
│ ├── StepNumber (VO: int) - Sequentiell
│ ├── Description (VO: String)
│ ├── DurationMinutes (VO: Integer) - Optional, geschätzte Dauer
│ └── TemperatureCelsius (VO: Integer) - Optional, für Räuchern/Kochen
└── Status (VO: DRAFT | ACTIVE | ARCHIVED)
Invarianten:
/**
* Recipe aggregate root.
*
* Invariants:
* - ACTIVE recipe must have at least one ingredient
* - YieldPercentage must be between 1-200% (>100% bei Pökel-/Spritzprozessen)
* - Ingredient quantities must be positive
* - Nested recipes (SubRecipeId) cannot create circular dependencies
* - Position numbers in Ingredients must be unique and sequential
* - StepNumber in ProductionSteps must be sequential (1, 2, 3, ...)
* - ShelfLifeDays must be > 0 for FINISHED_PRODUCT and INTERMEDIATE
* - Only DRAFT recipes can be modified (add/remove ingredients, steps)
* - Version is immutable after creation; new version = new Recipe
* - Status transitions: DRAFT → ACTIVE → ARCHIVED (no way back)
*/
Draft-Records:
public record RecipeDraft(
String articleId,
String name,
String recipeType, // RAW_MATERIAL | INTERMEDIATE | FINISHED_PRODUCT
Integer yieldPercentage,
Integer shelfLifeDays
) {}
public record IngredientDraft(
int position,
String articleId,
String quantityAmount, // BigDecimal als String
String quantityUnit, // UnitOfMeasure
String subRecipeId, // nullable
boolean substitutable
) {}
public record ProductionStepDraft(
int stepNumber,
String description,
Integer durationMinutes, // nullable
Integer temperatureCelsius // nullable
) {}
Factory & Business Methods:
// Factory
public static Result<RecipeError, Recipe> create(RecipeDraft draft);
// Mutations (nur im DRAFT-Status)
public Result<RecipeError, Void> addIngredient(IngredientDraft draft);
public Result<RecipeError, Void> removeIngredient(IngredientId ingredientId);
public Result<RecipeError, Void> addProductionStep(ProductionStepDraft draft);
public Result<RecipeError, Void> removeProductionStep(int stepNumber);
// Status-Übergänge
public Result<RecipeError, Void> activate();
public Result<RecipeError, Void> archive();
// Query Methods
public Quantity calculateRequiredInput(Quantity desiredOutput);
public LocalDate calculateBestBeforeDate(LocalDate productionDate);
public List<ArticleId> getAllIngredientArticleIds();
public boolean containsSubRecipe();
Domain Events:
RecipeCreated(RecipeId, ArticleId, RecipeType)
RecipeActivated(RecipeId)
RecipeArchived(RecipeId)
2. Batch (Aggregate Root)
Verantwortung: Repräsentiert eine Produktions-Charge mit lückenloser Rückverfolgbarkeit (Chargen-Genealogie). Zentral für HACCP-Compliance und Rückruf-Szenarien.
Batch (Aggregate Root)
├── BatchId (VO)
├── BatchNumber (VO) - Formatierte Chargennummer (z.B. "P-2026-02-17-001")
├── RecipeId (VO) - Referenz auf verwendete Rezeptur
├── ArticleId (VO) - Referenz auf produziertes Endprodukt
├── PlannedQuantity (VO: Quantity) - Soll-Menge
├── ActualQuantity (VO: Quantity) - Ist-Menge nach Rückmeldung
├── Waste (VO: Quantity) - Ausschuss/Schwund
├── ProductionDate (VO: LocalDate)
├── BestBeforeDate (VO: LocalDate) - MHD, berechnet aus Recipe.ShelfLifeDays
├── Status (VO: PLANNED | IN_PRODUCTION | COMPLETED | CANCELLED)
├── ProducedBy (VO: UserId)
├── BranchId (VO) - Produktionsstandort
├── Consumptions[] (Entity) - Chargen-Genealogie (Input-Tracking)
│ ├── ConsumptionId (VO)
│ ├── InputBatchId (VO) - Verbrauchte Rohstoff-/Zwischen-Charge
│ ├── ArticleId (VO) - Zutat-Artikel
│ ├── QuantityUsed (VO: Quantity) - Verbrauchte Menge (mit Catch-Weight)
│ └── ConsumedAt (VO: Instant)
├── Remarks (VO: String) - Optional, für Abweichungen
├── CancelledReason (VO: String) - Optional, Grund bei Stornierung
├── StartedAt (VO: Instant)
└── CompletedAt (VO: Instant)
Invarianten:
/**
* Batch aggregate root (ProductionBatch).
*
* Invariants:
* - PlannedQuantity must be positive
* - BestBeforeDate must be after ProductionDate
* - Status transitions: PLANNED → IN_PRODUCTION → COMPLETED
* PLANNED → CANCELLED
* IN_PRODUCTION → CANCELLED
* - Cannot complete without at least one Consumption recorded
* - ActualQuantity must be positive at completion
* - Waste must be >= 0
* - All Consumptions must reference distinct InputBatchIds
* - Consumptions can only be added in IN_PRODUCTION status
* - CompletedAt must be after StartedAt
* - BatchNumber is immutable after creation
*/
Draft-Record:
public record BatchDraft(
String recipeId,
String articleId,
String plannedQuantityAmount, // BigDecimal als String
String plannedQuantityUnit, // UnitOfMeasure
String productionDate, // ISO LocalDate
String bestBeforeDate, // ISO LocalDate
String producedBy, // UserId
String branchId
) {}
public record ConsumptionDraft(
String inputBatchId,
String articleId,
String quantityAmount, // BigDecimal als String
String quantityUnit // UnitOfMeasure
) {}
Factory & Business Methods:
// Factory
public static Result<BatchError, Batch> plan(BatchDraft draft);
// Status-Übergänge
public Result<BatchError, Void> startProduction();
public Result<BatchError, Void> complete(Quantity actualQuantity, Quantity waste, String remarks);
public Result<BatchError, Void> cancel(String reason);
// Genealogie (nur in IN_PRODUCTION)
public Result<BatchError, Void> recordConsumption(ConsumptionDraft draft);
// Query Methods
public boolean isFullyTraceable();
public List<BatchId> getUpstreamBatchIds();
public Duration getProductionDuration();
Domain Events:
BatchPlanned(BatchId, BatchNumber, RecipeId, ArticleId, Quantity)
BatchStarted(BatchId)
BatchCompleted(BatchId, ArticleId, Quantity, LocalDate bestBeforeDate, BranchId)
→ triggers Inventory stock-in
BatchCancelled(BatchId, String reason)
ConsumptionRecorded(BatchId, BatchId inputBatchId, Quantity)
→ triggers Inventory stock-out for consumed materials
3. ProductionOrder (Aggregate Root)
Verantwortung: Plant zukünftige Produktionen. Verbindet Produktionsplanung mit der konkreten Chargen-Erzeugung.
ProductionOrder (Aggregate Root)
├── ProductionOrderId (VO)
├── RecipeId (VO)
├── ArticleId (VO) - Zu produzierender Artikel
├── PlannedQuantity (VO: Quantity)
├── PlannedDate (VO: LocalDate)
├── Priority (VO: LOW | NORMAL | HIGH | URGENT)
├── Status (VO: PLANNED | RELEASED | IN_PRODUCTION | COMPLETED | CANCELLED)
├── CreatedBy (VO: UserId)
├── CreatedAt (VO: Instant)
├── BranchId (VO) - Ziel-Produktionsstandort
├── BatchId (VO) - Verknüpfung zur erzeugten Charge (nach Start)
├── Remarks (VO: String) - Optional
└── CancelledReason (VO: String) - Optional
Invarianten:
/**
* ProductionOrder aggregate root.
*
* Invariants:
* - PlannedQuantity must be positive
* - PlannedDate cannot be in the past at creation
* - Status transitions: PLANNED → RELEASED → IN_PRODUCTION → COMPLETED
* PLANNED → CANCELLED
* RELEASED → CANCELLED
* - Can only release if RecipeId references an ACTIVE recipe (checked in Application Layer)
* - Cannot start production without setting BatchId
* - Cannot complete without linked Batch being COMPLETED (checked in Application Layer)
* - BatchId is set exactly once (at startProduction)
* - Reschedule only allowed in PLANNED or RELEASED status
*/
Draft-Record:
public record ProductionOrderDraft(
String recipeId,
String articleId,
String plannedQuantityAmount, // BigDecimal als String
String plannedQuantityUnit, // UnitOfMeasure
String plannedDate, // ISO LocalDate
String priority, // LOW | NORMAL | HIGH | URGENT
String branchId,
String createdBy,
String remarks // nullable
) {}
Factory & Business Methods:
// Factory
public static Result<ProductionOrderError, ProductionOrder> create(ProductionOrderDraft draft);
// Status-Übergänge
public Result<ProductionOrderError, Void> release();
public Result<ProductionOrderError, Void> startProduction(BatchId batchId);
public Result<ProductionOrderError, Void> complete();
public Result<ProductionOrderError, Void> cancel(String reason);
// Mutations
public Result<ProductionOrderError, Void> reschedule(LocalDate newDate);
Domain Events:
ProductionOrderCreated(ProductionOrderId, RecipeId, Quantity, LocalDate)
ProductionOrderReleased(ProductionOrderId)
→ triggers material availability check / Procurement demand
ProductionOrderStarted(ProductionOrderId, BatchId)
ProductionOrderCompleted(ProductionOrderId, BatchId)
ProductionOrderCancelled(ProductionOrderId, String reason)
ProductionOrderRescheduled(ProductionOrderId, LocalDate oldDate, LocalDate newDate)
Shared Value Objects
---
config:
theme: neutral
look: classic
layout: elk
themeVariables:
background: "#f8fafc"
class:
hideEmptyMembersBox: true
---
classDiagram
class Quantity {
+BigDecimal amount
+UnitOfMeasure uom
+BigDecimal secondaryAmount
+UnitOfMeasure secondaryUom
+of(BigDecimal, UnitOfMeasure) Result
+dual(BigDecimal, UnitOfMeasure, BigDecimal, UnitOfMeasure) Result
+hasDualQuantity() boolean
+add(Quantity) Quantity
+subtract(Quantity) Quantity
+multiply(BigDecimal) Quantity
+isZero() boolean
+isPositive() boolean
}
class UnitOfMeasure {
<<enumeration>>
KILOGRAM
GRAM
LITER
MILLILITER
PIECE
METER
}
class YieldPercentage {
+int value
+calculateRequiredInput(Quantity) Quantity
}
class BatchNumber {
+String value
+generate(LocalDate, int) BatchNumber
}
class RecipeType {
<<enumeration>>
RAW_MATERIAL
INTERMEDIATE
FINISHED_PRODUCT
}
class Priority {
<<enumeration>>
LOW
NORMAL
HIGH
URGENT
}
class RecipeStatus {
<<enumeration>>
DRAFT
ACTIVE
ARCHIVED
}
class BatchStatus {
<<enumeration>>
PLANNED
IN_PRODUCTION
COMPLETED
CANCELLED
}
class ProductionOrderStatus {
<<enumeration>>
PLANNED
RELEASED
IN_PRODUCTION
COMPLETED
CANCELLED
}
Quantity --> UnitOfMeasure
Quantity (mit Catch-Weight / Dual-Quantity)
Zentral für die gesamte Produktion. Unterstützt Dual-Quantity für Catch-Weight-Artikel (z.B. "10 Stück à 2,3 kg").
public record Quantity(
BigDecimal amount,
UnitOfMeasure uom,
BigDecimal secondaryAmount, // Catch-Weight, nullable
UnitOfMeasure secondaryUom // nullable
) {
// Factory: einfache Menge
public static Result<QuantityError, Quantity> of(BigDecimal amount, UnitOfMeasure uom);
// Factory: Dual-Quantity (Catch-Weight)
public static Result<QuantityError, Quantity> dual(
BigDecimal primaryAmount, UnitOfMeasure primaryUom,
BigDecimal secondaryAmount, UnitOfMeasure secondaryUom
);
public boolean hasDualQuantity();
public Quantity add(Quantity other);
public Quantity subtract(Quantity other);
public Quantity multiply(BigDecimal factor);
public boolean isZero();
public boolean isPositive();
}
UnitOfMeasure
public enum UnitOfMeasure {
KILOGRAM("kg"),
GRAM("g"),
LITER("l"),
MILLILITER("ml"),
PIECE("Stk"),
METER("m");
private final String abbreviation;
}
BatchNumber
public record BatchNumber(String value) {
// Format: "P-YYYY-MM-DD-XXX" (P = Production)
public static BatchNumber generate(LocalDate productionDate, int sequenceNumber) {
String value = String.format("P-%s-%03d", productionDate, sequenceNumber);
return new BatchNumber(value);
}
}
YieldPercentage
public record YieldPercentage(int value) {
public YieldPercentage {
if (value < 1 || value > 200) {
throw new IllegalArgumentException(
"Yield percentage must be between 1-200, got: " + value
);
}
}
/**
* Berechnet benötigten Input für gewünschten Output.
* Bei 80% Ausbeute und 100kg gewünschtem Output → 125kg Input nötig.
*/
public Quantity calculateRequiredInput(Quantity desiredOutput) {
return desiredOutput.multiply(new BigDecimal(100).divide(
new BigDecimal(value), 4, RoundingMode.HALF_UP));
}
}
Priority
public enum Priority {
LOW,
NORMAL,
HIGH,
URGENT
}
Domain Services
RecipeCycleDependencyChecker
/**
* Prüft ob verschachtelte Rezepturen zirkuläre Abhängigkeiten erzeugen.
* Wird im Application Layer aufgerufen nach Recipe.addIngredient() mit SubRecipeId.
*/
public class RecipeCycleDependencyChecker {
public Result<RecipeError, Void> check(RecipeId recipeId, RecipeRepository repository);
}
BatchTraceabilityService
/**
* Rückverfolgbarkeits-Service für HACCP-Compliance und Rückruf-Szenarien.
* Traversiert Chargen-Genealogie über BatchConsumptions.
*/
public class BatchTraceabilityService {
/**
* Vorwärts-Tracing: Welche Endprodukt-Chargen verwenden diese Rohstoff-Charge?
* KRITISCH für Rückruf-Szenarien.
*/
public List<BatchId> traceForward(BatchId sourceBatchId);
/**
* Rückwärts-Tracing: Welche Rohstoff-Chargen stecken in diesem Endprodukt?
*/
public List<BatchId> traceBackward(BatchId targetBatchId);
}
Chargen-Genealogie (Beispiel):
---
config:
theme: neutral
look: classic
layout: elk
themeVariables:
background: "#f8fafc"
---
graph TB
subgraph ROHSTOFFE["Rohstoff-Chargen (Wareneingang)"]
RB1["P-2026-02-10-001\nSchweineschulter\n50kg, MHD 2026-03-01"]
RB2["P-2026-02-11-003\nGewürzmischung A\n5kg, MHD 2026-08-01"]
RB3["P-2026-02-10-007\nNitritpökelsalz\n2kg, MHD 2027-01-01"]
end
subgraph ZWISCHEN["Zwischen-Charge"]
ZB1["P-2026-02-15-001\nBrät (Intermediate)\n40kg, MHD 2026-02-18"]
end
subgraph ENDPRODUKT["Endprodukt-Chargen"]
EB1["P-2026-02-15-002\nFleischwurst\n35kg, MHD 2026-03-15"]
end
RB1 -->|"Consumption\n45kg"| ZB1
RB2 -->|"Consumption\n3kg"| ZB1
RB3 -->|"Consumption\n1.5kg"| ZB1
ZB1 -->|"Consumption\n38kg"| EB1
style RB1 fill:#e3f2fd
style RB2 fill:#e3f2fd
style RB3 fill:#e3f2fd
style ZB1 fill:#fff3e0
style EB1 fill:#e8f5e9
EB1 -. "Rückwärts-Tracing\n(traceBackward)" .-> ZB1
ZB1 -. "Rückwärts-Tracing" .-> RB1
RB1 -. "Vorwärts-Tracing\n(traceForward)" .-> ZB1
ZB1 -. "Vorwärts-Tracing" .-> EB1
BatchNumberGenerator
/**
* Generiert eindeutige Chargennummern. Infrastructure-Concern (Sequenz aus DB).
*/
public interface BatchNumberGenerator {
BatchNumber generate(LocalDate productionDate);
}
Domain Errors
public sealed interface RecipeError {
String message();
record RecipeNotFound(String recipeId) implements RecipeError {
public String message() { return "Recipe not found: " + recipeId; }
}
record InvalidYieldPercentage(int value) implements RecipeError {
public String message() { return "Yield must be 1-200%, got: " + value; }
}
record CyclicDependencyDetected(String path) implements RecipeError {
public String message() { return "Cyclic recipe dependency: " + path; }
}
record NoIngredients() implements RecipeError {
public String message() { return "Recipe must have at least one ingredient"; }
}
record DuplicatePosition(int position) implements RecipeError {
public String message() { return "Duplicate ingredient position: " + position; }
}
record NotInDraftStatus() implements RecipeError {
public String message() { return "Recipe can only be modified in DRAFT status"; }
}
record InvalidShelfLife(int days) implements RecipeError {
public String message() { return "Shelf life must be > 0, got: " + days; }
}
record InvalidQuantity(String reason) implements RecipeError {
public String message() { return "Invalid quantity: " + reason; }
}
}
public sealed interface BatchError {
String message();
record InvalidQuantity(String reason) implements BatchError {
public String message() { return "Invalid quantity: " + reason; }
}
record InvalidStatusTransition(String from, String to) implements BatchError {
public String message() { return "Cannot transition from " + from + " to " + to; }
}
record MissingConsumptions() implements BatchError {
public String message() { return "Cannot complete batch without recorded consumptions"; }
}
record InvalidBestBeforeDate() implements BatchError {
public String message() { return "Best-before date must be after production date"; }
}
record DuplicateInputBatch(String batchId) implements BatchError {
public String message() { return "Input batch already recorded: " + batchId; }
}
record NegativeWaste() implements BatchError {
public String message() { return "Waste cannot be negative"; }
}
}
public sealed interface ProductionOrderError {
String message();
record PlannedDateInPast(String date) implements ProductionOrderError {
public String message() { return "Planned date is in the past: " + date; }
}
record InvalidQuantity(String reason) implements ProductionOrderError {
public String message() { return "Invalid quantity: " + reason; }
}
record InvalidStatusTransition(String from, String to) implements ProductionOrderError {
public String message() { return "Cannot transition from " + from + " to " + to; }
}
record BatchAlreadyAssigned(String batchId) implements ProductionOrderError {
public String message() { return "Batch already assigned: " + batchId; }
}
}
Repository Interfaces
public interface RecipeRepository {
Optional<Recipe> findById(RecipeId id);
List<Recipe> findByStatus(RecipeStatus status);
List<Recipe> findByArticleId(ArticleId articleId);
List<Recipe> findActiveByIngredientArticleId(ArticleId ingredientArticleId);
void save(Recipe recipe);
boolean existsByNameAndVersion(String name, int version);
}
public interface BatchRepository {
Optional<Batch> findById(BatchId id);
Optional<Batch> findByBatchNumber(BatchNumber batchNumber);
List<Batch> findByProductionDate(LocalDate date);
List<Batch> findByStatus(BatchStatus status);
List<Batch> findByArticleId(ArticleId articleId);
// Für Rückverfolgbarkeit
List<Batch> findByInputBatchId(BatchId upstreamBatchId);
void save(Batch batch);
}
public interface ProductionOrderRepository {
Optional<ProductionOrder> findById(ProductionOrderId id);
List<ProductionOrder> findByPlannedDate(LocalDate date);
List<ProductionOrder> findByPlannedDateRange(LocalDate from, LocalDate to);
List<ProductionOrder> findByStatus(ProductionOrderStatus status);
void save(ProductionOrder order);
}
Status-Maschinen
Recipe Status
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
stateDiagram-v2
[*] --> DRAFT : create()
DRAFT --> ACTIVE : activate()
ACTIVE --> ARCHIVED : archive()
DRAFT : Editierbar
DRAFT : addIngredient(), removeIngredient()
DRAFT : addProductionStep(), removeProductionStep()
ACTIVE : Nur lesen, in Produktion verwendbar
ARCHIVED : Nur lesen, nicht mehr verwendbar
Batch Status
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
stateDiagram-v2
[*] --> PLANNED : plan()
PLANNED --> IN_PRODUCTION : startProduction()
PLANNED --> CANCELLED : cancel()
IN_PRODUCTION --> COMPLETED : complete()
IN_PRODUCTION --> CANCELLED : cancel()
PLANNED : Charge geplant
IN_PRODUCTION : recordConsumption()
IN_PRODUCTION : Genealogie dokumentieren
COMPLETED : actualQuantity + waste gesetzt
COMPLETED : → BatchCompleted Event
CANCELLED : cancelledReason gesetzt
ProductionOrder Status
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
stateDiagram-v2
[*] --> PLANNED : create()
PLANNED --> RELEASED : release()
PLANNED --> CANCELLED : cancel()
RELEASED --> IN_PRODUCTION : startProduction(batchId)
RELEASED --> CANCELLED : cancel()
IN_PRODUCTION --> COMPLETED : complete()
PLANNED : reschedule() möglich
RELEASED : Material-Check bestanden
RELEASED : reschedule() möglich
RELEASED : → ProductionOrderReleased Event
IN_PRODUCTION : BatchId zugewiesen
COMPLETED : Batch ist COMPLETED
Integration mit anderen BCs
---
config:
theme: neutral
look: classic
layout: elk
themeVariables:
background: "#f8fafc"
---
graph LR
subgraph UPSTREAM["Upstream BCs"]
MD["Master Data\n(ArticleId)"]
UM["User Management\n(UserId)"]
FI["Filiales\n(BranchId)"]
end
subgraph PRODUCTION["Production BC"]
R["Recipe"]
B["Batch"]
PO["ProductionOrder"]
end
subgraph DOWNSTREAM["Downstream BCs"]
INV["Inventory BC"]
PROC["Procurement BC"]
LAB["Labeling BC"]
end
MD -->|ArticleId| R
MD -->|ArticleId| B
MD -->|ArticleId| PO
UM -->|UserId| B
UM -->|UserId| PO
FI -->|BranchId| B
FI -->|BranchId| PO
PO -->|erzeugt| B
R -->|RecipeId| B
R -->|RecipeId| PO
B -->|"BatchCompleted\n(stock-in)"| INV
B -->|"ConsumptionRecorded\n(stock-out)"| INV
PO -->|"OrderReleased\n(demand)"| PROC
R -->|"RecipeActivated\n(Nährwerte)"| LAB
Upstream-Abhängigkeiten (Production konsumiert)
| BC | Referenz | Zweck |
|---|---|---|
| Master Data | ArticleId | Artikel-Referenz in Recipe, Batch, ProductionOrder |
| User Management | UserId | ProducedBy in Batch, CreatedBy in ProductionOrder |
| Filiales | BranchId | Produktionsstandort |
Downstream-Integrationen (Production publiziert Events)
| Event | Konsument | Aktion |
|---|---|---|
BatchCompleted |
Inventory BC | Stock-In für produzierte Ware |
ConsumptionRecorded |
Inventory BC | Stock-Out für verbrauchte Rohstoffe |
ProductionOrderReleased |
Procurement BC | Materialbedarf / Demand Planning |
RecipeActivated |
Labeling BC | Nährwert-/Allergen-Berechnung |
Abgrenzungen (gehören NICHT in Production BC)
| Konzept | Zuständiger BC | Grund |
|---|---|---|
| QualityHold / Freigabe | Quality BC | Eigene Aggregate-Logik mit QualityStatus |
| StockReservation | Inventory BC | Bestandsführung ist Inventory-Concern |
| Labeling / Deklaration | Labeling BC | Nährwert- und Allergen-Deklaration |
| Lieferanten-Chargen | Master Data BC | SupplierBatchNumber ist Stammdaten-Concern |
Use Cases (Application Layer)
// Recipe Management
CreateRecipe → Recipe.create(RecipeDraft)
AddRecipeIngredient → recipe.addIngredient(IngredientDraft)
ActivateRecipe → recipe.activate()
ArchiveRecipe → recipe.archive()
GetRecipe → Query
ListActiveRecipes → Query
// Batch Management
PlanBatch → Batch.plan(BatchDraft)
StartBatch → batch.startProduction()
RecordConsumption → batch.recordConsumption(ConsumptionDraft)
CompleteBatch → batch.complete(actualQty, waste, remarks)
CancelBatch → batch.cancel(reason)
TraceBatchForward → BatchTraceabilityService.traceForward(batchId)
TraceBatchBackward → BatchTraceabilityService.traceBackward(batchId)
// ProductionOrder Management
CreateProductionOrder → ProductionOrder.create(ProductionOrderDraft)
ReleaseOrder → order.release()
StartOrderProduction → order.startProduction(batchId) + Batch.plan()
CompleteOrder → order.complete()
CancelOrder → order.cancel(reason)
RescheduleOrder → order.reschedule(newDate)
ListOrdersByDate → Query
Beispiel: Produktionsfluss (End-to-End)
TODO: Was passiert wenn Material da/nicht da? Woher ist klar dass Produktion gestartet werden kann?
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
sequenceDiagram
participant M as Metzgermeister
participant PO as ProductionOrder
participant R as Recipe
participant B as Batch
participant INV as Inventory BC
participant PROC as Procurement BC
M->>PO: create(ProductionOrderDraft)
activate PO
PO-->>M: ProductionOrder (PLANNED)
M->>PO: release()
PO--)PROC: ProductionOrderReleased Event
Note over PROC: Materialbedarf prüfen
M->>PO: startProduction(batchId)
PO->>B: plan(BatchDraft)
activate B
B-->>PO: Batch (PLANNED)
B->>B: startProduction()
Note over B: Status: IN_PRODUCTION
M->>B: recordConsumption(Schweineschulter, 125kg)
B--)INV: ConsumptionRecorded Event
Note over INV: Stock-Out: 125kg Schweineschulter
M->>B: recordConsumption(Gewürz, 3kg)
B--)INV: ConsumptionRecorded Event
M->>B: complete(80kg, 5kg waste)
B--)INV: BatchCompleted Event
Note over INV: Stock-In: 80kg Endprodukt
deactivate B
M->>PO: complete()
deactivate PO
Note over PO: Status: COMPLETED
Code-Beispiel
// 1. Rezeptur existiert (ACTIVE)
Recipe recipe = recipeRepository.findById(recipeId).orElseThrow();
// 2. Produktionsauftrag erstellen
var orderDraft = new ProductionOrderDraft(
recipeId, articleId, "100", "KILOGRAM",
"2026-02-20", "NORMAL", branchId, userId, null
);
ProductionOrder order = ProductionOrder.create(orderDraft).value();
// 3. Auftrag freigeben (Material-Check im Application Layer)
order.release();
// 4. Produktion starten → Charge erzeugen
var batchDraft = new BatchDraft(
recipeId, articleId, "100", "KILOGRAM",
"2026-02-20", "2026-03-22", // MHD = ProductionDate + ShelfLifeDays
userId, branchId
);
Batch batch = Batch.plan(batchDraft).value();
batch.startProduction();
order.startProduction(batch.id());
// 5. Rohstoff-Verbrauch dokumentieren (Genealogie)
batch.recordConsumption(new ConsumptionDraft(
"P-2026-02-15-042", // Rohstoff-Charge
"ART-001", // Schweineschulter
"125", "KILOGRAM" // 125kg Input für 100kg Output bei 80% Ausbeute
));
// 6. Charge abschließen
batch.complete(
Quantity.of(new BigDecimal("80"), UnitOfMeasure.KILOGRAM).value(),
Quantity.of(new BigDecimal("5"), UnitOfMeasure.KILOGRAM).value(),
"Leichter Schwund beim Räuchern"
);
order.complete();
// 7. Events:
// BatchCompleted → Inventory bucht 80kg Endprodukt ein
// ConsumptionRecorded → Inventory bucht 125kg Rohstoff aus
DDD Validation Checklist
- Aggregate Root ist einziger Einstiegspunkt (Recipe, Batch, ProductionOrder)
- Alle Änderungen gehen durch Aggregate-Root-Methoden
- Invarianten werden in Factory und Methoden geprüft
- Keine direkten Referenzen auf andere Aggregates (nur IDs: ArticleId, UserId, BranchId, BatchId)
- Ein Aggregate = eine Transaktionsgrenze
- EntityDraft-Pattern für VO-Konstruktion im Domain Layer
- Result<E,T> für erwartbare Fehler, keine Exceptions
- Sealed interfaces für Domain Errors
- Status-Maschinen explizit dokumentiert
- BC-Grenzen klar definiert (QualityHold → Quality BC, Stock → Inventory BC)