Appendix B: When to Create New Patterns¶
This appendix helps you decide when to create new entities, contexts, or event types.
Decision 1: New Entity vs. New Field¶
Question: Should I add a field to an existing entity, or create a new entity?
flowchart TD
Q1{Does it need its<br/>own identity?}
Q2{Different lifecycle<br/>from parent?}
Q3{Relates to multiple<br/>other entities?}
Q4{Complex validation<br/>rules of its own?}
FIELD[Add FIELD to<br/>existing entity]
ENTITY[Create new ENTITY]
Q1 -->|Yes| ENTITY
Q1 -->|No| Q2
Q2 -->|Yes| ENTITY
Q2 -->|No| Q3
Q3 -->|Yes| ENTITY
Q3 -->|No| Q4
Q4 -->|Yes| ENTITY
Q4 -->|No| FIELD
Add a Field When:¶
- The data belongs to the same aggregate root
- It shares the same lifecycle (created/deleted together)
- Validation rules are related to existing rules
- It doesn't introduce a new identity
Example: Adding priority to Code
# Good: Priority is an attribute of Code
@dataclass(frozen=True)
class Code:
id: CodeId
name: str
color: Color
priority: Optional[int] = None # Just add the field
Create a New Entity When:¶
- It has its own identity (needs its own ID)
- It has a different lifecycle
- It relates to multiple other entities
- It has complex validation rules of its own
Example: Tags (many-to-many with Codes)
# Good: Tag is its own entity
@dataclass(frozen=True)
class Tag:
id: TagId # Own identity
name: str
color: Color
@dataclass(frozen=True)
class CodeTag: # Junction entity
code_id: CodeId
tag_id: TagId
Not just a field on Code, because: - Tags can exist independently - One Tag can be on multiple Codes - Tags have their own CRUD operations
Decision 2: New Bounded Context vs. Extending Existing¶
Question: Should this feature be a new bounded context, or part of an existing one?
flowchart TD
Q1{Uses same<br/>core entities?}
Q2{Same ubiquitous<br/>language?}
Q3{Same team<br/>works on it?}
Q4{Changes coordinated<br/>together?}
EXTEND[Extend existing<br/>context]
NEW[Create new<br/>bounded context]
Q1 -->|No| NEW
Q1 -->|Yes| Q2
Q2 -->|No| NEW
Q2 -->|Yes| Q3
Q3 -->|No| NEW
Q3 -->|Yes| Q4
Q4 -->|No| NEW
Q4 -->|Yes| EXTEND
Extend Existing Context When:¶
- The new feature uses the same core entities
- It shares the same ubiquitous language
- Changes would be coordinated together
- The team working on it is the same
Example: Adding priority to Coding context
Priority is part of how researchers organize codes. It uses Code entities and the same language ("high priority codes").
Create New Context When:¶
- It has distinct entities and language
- Different teams might work on it
- It could be deployed/scaled independently
- It only needs to react to other contexts' events
Example: Reports context
graph LR
subgraph Coding ["Coding Context"]
Code[Code]
Segment[Segment]
Category[Category]
end
subgraph Reports ["Reports Context"]
Report[Report]
Chart[Chart]
Template[ReportTemplate]
end
Coding -->|events| Reports
Reports: - Has its own entities (Report, ReportTemplate, Chart) - Uses different language ("generate report", "export PDF") - Can be developed independently - Just subscribes to Coding events
Decision 3: New Event Type vs. Reusing Existing¶
Question: Should I create a new event type, or can I reuse/extend an existing one?
flowchart TD
Q1{Distinct business<br/>action?}
Q2{Subscribers handle<br/>differently?}
Q3{Different required<br/>fields?}
EXTEND[Extend existing<br/>event with optional field]
NEW[Create new<br/>event type]
Q1 -->|Yes| NEW
Q1 -->|No| Q2
Q2 -->|Yes| NEW
Q2 -->|No| Q3
Q3 -->|Yes| NEW
Q3 -->|No| EXTEND
Reuse/Extend When:¶
- It's a variation of the same business action
- Subscribers would handle it the same way
- The extra data is optional
Example: Adding priority to CodeCreated
# Good: Just add the field
@dataclass(frozen=True)
class CodeCreated(DomainEvent):
code_id: CodeId
name: str
color: Color
priority: Optional[int] = None # Optional, backwards compatible
Existing subscribers still work (they ignore priority).
Create New Event When:¶
- It represents a distinct business action
- Subscribers need to handle it differently
- It has different required fields
Example: CodePriorityChanged
# Good: Distinct action, distinct event
@dataclass(frozen=True)
class CodePriorityChanged(DomainEvent):
code_id: CodeId
old_priority: Optional[int]
new_priority: Optional[int]
This is a separate action from creating a code. UI might: - Show "Priority changed" in activity feed - Update priority indicator without full refresh - Trigger priority-based notifications
Decision 4: New Invariant vs. Inline Check¶
Question: Should I create a named invariant, or just check inline?
flowchart TD
Q1{Rule reused in<br/>multiple places?}
Q2{Has business meaning<br/>worth naming?}
Q3{Should be tested<br/>independently?}
INLINE[Inline check<br/>is fine]
NAMED[Create named<br/>invariant]
Q1 -->|Yes| NAMED
Q1 -->|No| Q2
Q2 -->|Yes| NAMED
Q2 -->|No| Q3
Q3 -->|Yes| NAMED
Q3 -->|No| INLINE
Create Named Invariant When:¶
- The rule is reused in multiple places
- The rule has business meaning worth naming
- It helps documentation/understanding
- It should be tested independently
Example: is_valid_priority
# Good: Named, reusable, testable
def is_valid_priority(priority: Optional[int]) -> bool:
"""Priority must be 1-5, or None."""
if priority is None:
return True
return 1 <= priority <= 5
Used in:
- derive_create_code
- derive_update_code_priority
- Potentially in UI validation
Inline Check When:¶
- It's a one-off, context-specific check
- The logic is trivial and obvious
- It would clutter the invariants module
Example: Simple null check
# Fine: Inline for obvious checks
if category_id is not None:
if not does_category_exist(category_id, state.existing_categories):
return CodeNotCreated.category_not_found(category_id)
The category_id is not None check doesn't need a named invariant.
Decision 5: New Failure Event Reason vs. Reusing Existing¶
Question: Should I create a new failure event reason, or reuse an existing one?
flowchart TD
Q1{Error has specific<br/>context to capture?}
Q2{UI needs to handle<br/>it differently?}
Q3{Message needs<br/>specific field values?}
GENERIC[Reuse generic<br/>failure type]
SPECIFIC[Create new<br/>failure type]
Q1 -->|Yes| SPECIFIC
Q1 -->|No| Q2
Q2 -->|Yes| SPECIFIC
Q2 -->|No| Q3
Q3 -->|Yes| SPECIFIC
Q3 -->|No| GENERIC
Create New Failure Event Reason When:¶
- The error has specific context to capture
- UI needs to handle it differently
- It helps debugging/logging
- The message needs specific field values
Example: CodeNotCreated.invalid_priority()
@dataclass(frozen=True)
class CodeNotCreated(FailureEvent):
priority_value: int | None = None # Capture the bad value
@classmethod
def invalid_priority(cls, value: int) -> "CodeNotCreated":
return cls(
event_type="CODE_NOT_CREATED/INVALID_PRIORITY",
priority_value=value,
...
)
@property
def message(self) -> str:
if self.reason == "INVALID_PRIORITY":
return f"Priority must be 1-5, got {self.priority_value}"
...
UI can show: "Priority must be 1-5, got 10" with the actual bad value.
Reuse Existing Failure Event Reason When:¶
- The error is truly generic
- No specific context helps
- It's a rare edge case
Example: Generic validation error
# Sometimes fine for rare cases - use a generic reason
return CodeNotCreated(
event_type="CODE_NOT_CREATED/VALIDATION_ERROR",
...
)
But usually, a specific reason is better for debugging and UI handling.
Master Decision Tree¶
flowchart TD
START[Adding new<br/>functionality?]
START --> Q1{Needs own<br/>identity/lifecycle?}
Q1 -->|Yes| ENTITY[Create new<br/>ENTITY]
Q1 -->|No| FIELD[Add FIELD to<br/>existing entity]
START --> Q2{Distinct language<br/>and entities?}
Q2 -->|Yes| CONTEXT[Create new<br/>BOUNDED CONTEXT]
Q2 -->|No| EXTEND_CTX[Extend existing<br/>context]
START --> Q3{Distinct business<br/>action?}
Q3 -->|Yes| EVENT[Create new<br/>EVENT TYPE]
Q3 -->|No| EXTEND_EVT[Add optional field<br/>to existing event]
START --> Q4{Validation rule<br/>reused?}
Q4 -->|Yes| INVARIANT[Create named<br/>INVARIANT]
Q4 -->|No| INLINE[Inline check<br/>is fine]
START --> Q5{Error has<br/>specific context?}
Q5 -->|Yes| FAILURE[Create new<br/>FAILURE TYPE]
Q5 -->|No| GENERIC[Reuse generic<br/>failure]
Practical Examples¶
Example 1: Adding "Due Date" to Codes¶
Analysis:
- Due date is an attribute of Code (same lifecycle) → Add field
- Still Coding context (organizing codes) → Extend existing
- Setting due date is distinct from creating → New event (CodeDueDateSet)
- Due date validation is reusable → New invariant (is_valid_due_date)
- Invalid date has specific info → New failure (InvalidDueDate)
Example 2: Adding User Permissions¶
Analysis:
- Users have own identity and lifecycle → New entity (User, Permission)
- Permissions are cross-cutting, own language → New context (Auth)
- Each permission action is distinct → New events (PermissionGranted, etc.)
- Permission rules are complex → New invariants
- Permission errors need context → New failures
Example 3: Adding Code Description¶
Analysis:
- Description is an attribute of Code → Add field
- Same context → Extend existing
- Updating description is like memo → Extend CodeMemoUpdated or rename to CodeDescriptionUpdated
- Validation is simple (max length) → New invariant (is_valid_description)
- Empty/too long have context → New failures (DescriptionTooLong)
Summary¶
When in doubt:
- Start simple - Add a field before creating an entity
- Keep contexts focused - Don't merge unrelated concerns
- Name things explicitly - New event types are cheap
- Test drives design - If hard to test, probably wrong structure
- Ask the domain expert - Does this match how they think about it?
The fDDD architecture is flexible. You can always refactor as understanding grows.