DOMAIN:SECURITY:SECURE_DESIGN_PATTERNS¶
OWNER: koen, eric
ALSO_USED_BY: urszula, maxim, floris, floor, julian
UPDATED: 2026-03-19
SCOPE: all code reviews, all projects
SOURCE: Secure by Design (Johnsson/Deogun/Sawano, 2019)
CORE_PRINCIPLE¶
RULE: security is a CONCERN, not a FEATURE
IF security described as feature (login page, fraud module) THEN challenge — feature ≠ concern met
IF security concern THEN ask: "what must NEVER happen?" — then design against it
NOTE: security features address specific problems; security concerns cut across all functionality
ANALOGY: high-quality locks don't help if hinges are weak (Öst-Götha Bank robbery, 1854)
RULE: focus on DESIGN, not security — good design yields security implicitly
IF domain model precise THEN most malicious input rejected without security-specific code
IF developer thinks about domain constraints THEN XSS/injection often impossible by construction
ANTI_PATTERN: bolt-on security — adding validation after the fact
FIX: bake domain rules into type constructors from the start
SECURITY:CIA_T¶
STANDARD: Confidentiality, Integrity, Availability, Traceability
| concern | definition | breach example |
|---|---|---|
| confidentiality | keeping secrets secret | health records exposed |
| integrity | data only changes in authorized ways | election results manipulated |
| availability | data at hand when needed | fire dept can't get location |
| traceability | who accessed/changed what, when | GDPR Art. 30 audit requirement |
CHECK: for every data type, profile which CIA-T concern dominates
NOTE: health records: C breach = angry, I breach = dangerous, A breach = fatal
NOTE: bank records: C breach = angry, I breach = catastrophic, A breach = annoying
SECURITY:DOMAIN_PRIMITIVES¶
RULE: represent business concepts as TYPES, not strings
RULE: enforce domain invariants at construction time — invalid state is impossible
RULE: never accept generic types (String, int, Object) for domain-meaningful values
WHAT_IS_A_DOMAIN_PRIMITIVE¶
- smallest building block in domain model
- immutable value object with self-validation
- context-specific: Username, PhoneNumber, EmailAddress, OrderId — NOT String
- rejects invalid state at construction → all downstream code trusts validity
CONSTRUCTION_PATTERN¶
class Username {
constructor(value: string) {
// 1. null/blank check
// 2. length check (4-40 chars)
// 3. lexical content check (regex: [A-Za-z0-9_-]+)
// stored value is ALWAYS valid
}
}
CHECK: does the codebase use raw strings for business concepts?
CHECK: can a constructor be called with empty/null/malicious input and succeed?
ANTI_PATTERN: register(phoneNumber: string) — phoneNumber can be anything
FIX: register(phoneNumber: PhoneNumber) — PhoneNumber validates at construction
ANTI_PATTERN: security-specific validation (XSS filter library) bolted onto domain classes
FIX: domain constraints naturally exclude attack payloads — <script> is not a valid username
ANTI_PATTERN: repairing bad data before storing
FIX: NEVER repair input — reject and throw
SECURITY_BENEFIT¶
IF domain primitive has strict invariants THEN:
- XSS impossible (script tags not valid in domain)
- SQL injection impossible (special chars not valid in domain)
- buffer overflow mitigated (length bounded)
- type confusion impossible (Username cannot be used as Password)
NOTE: security achieved WITHOUT thinking about security — domain focus alone does it
READ_ALSO: domains/security/index.md (A03:INJECTION, A04:INSECURE_DESIGN)
SECURITY:VALIDATION_ORDER¶
RULE: validate input in this EXACT order
| step | check | purpose |
|---|---|---|
| 1 | null/blank | reject absent data first |
| 2 | origin | where did this data come from? trusted source? |
| 3 | size/length | reject oversized input before processing |
| 4 | lexical content | correct characters and encoding? |
| 5 | syntax | correct format/structure? |
| 6 | semantics | does it make sense in domain context? |
ANTI_PATTERN: running regex on unbounded input → ReDoS (regular expression denial of service)
FIX: length check BEFORE regex check
ANTI_PATTERN: checking semantics before syntax → parsing malformed data
FIX: follow the order — each step makes the next step safer
ANTI_PATTERN: trusting data origin without verification
FIX: check origin explicitly — is this from a trusted boundary?
NOTE: validation at system boundary only — internal code trusts domain primitives
SECURITY:IMMUTABILITY¶
RULE: prefer immutable objects — mutable state is the #1 source of security bugs
RULE: declare fields readonly/final — prevent mutation after construction
RULE: never return mutable internal state from getters
WHY_IMMUTABILITY_MATTERS¶
- thread safety: immutable objects need no synchronization
- temporal coupling eliminated: can't change object between check and use (TOCTOU)
- reasoning simplified: value at creation = value forever
- defensive copies unnecessary: share freely without fear
ANTI_PATTERN: getItems() returns internal list → caller can mutate entity state
FIX: return defensive copy or unmodifiable view
ANTI_PATTERN: mutable Date/DateTime fields exposed via getters
FIX: use immutable date types or return copies
CHECK: do entity getters return mutable collections?
CHECK: are fields that should be immutable missing readonly/final?
CHECK: are mutable objects shared between contexts without defensive copies?
SECURITY:ENTITY_INTEGRITY¶
RULE: entities must be consistent ON CREATION — no two-phase init
RULE: mandatory fields go in constructor — not setters
RULE: state changes via explicit methods, not setters
RULE: entity = identity + mutable state; protect both
CREATION_PATTERNS¶
ANTI_PATTERN: no-arg constructor + setter chain → entity exists in invalid state
FIX: constructor takes all mandatory fields; reject if missing
ANTI_PATTERN: builder with no validation → build() produces invalid entity
FIX: builder validates invariants in build() — throws if invalid
IF >3 constructor args THEN use builder pattern with validation
IF advanced cross-field constraints THEN enforce in builder.build()
PROTECTING_STATE¶
ANTI_PATTERN: public setter on entity field → any caller can corrupt state
FIX: expose domain methods: order.cancel() not order.setStatus("cancelled")
ANTI_PATTERN: getter returns mutable collection → external code mutates entity
FIX: return unmodifiable view: Collections.unmodifiableList(items)
CHECK: can entity state be changed to violate business rules via setters?
CHECK: do state transitions enforce preconditions?
ENTITY_COMPLEXITY_PATTERNS¶
ENTITY_STATE_OBJECT¶
WHEN: entity has complex state rules (status transitions, conditional fields)
PATTERN: extract state into immutable state object — entity delegates to it
BENEFIT: state rules testable independently, entity stays clean
ENTITY_SNAPSHOT¶
WHEN: entity is long-lived, state read frequently, mutations rare
PATTERN: immutable snapshot class reads entity state at point-in-time
BENEFIT: readers get thread-safe, consistent view; no locking needed
EXAMPLE: OrderSnapshot — immutable read of Order entity for display/reporting
ENTITY_RELAY¶
WHEN: entity lifecycle has distinct phases (draft → submitted → approved → completed)
PATTERN: separate class per phase — pass data between phases via immutable transfer
BENEFIT: each phase enforces only its own constraints; simpler code per phase
IF state graph too complex for one class THEN split into relay phases
READ_ONCE_OBJECTS¶
RULE: sensitive values (passwords, tokens, API keys) should be read-once
PATTERN: after first read, value is erased from memory
BENEFIT: prevents accidental logging, serialization, or reuse of sensitive data
CHECK: can sensitive values be logged by accident?
CHECK: can sensitive values be serialized (JSON.stringify) and leak?
READ_ALSO: domains/security/index.md (A04:INSECURE_DESIGN)
SECURITY:AGGREGATES_AND_BOUNDARIES¶
RULE: aggregate = consistency boundary — all invariants within aggregate are ALWAYS true
RULE: access aggregate only through root entity
RULE: cross-aggregate references by ID only, not object reference
CHECK: can child entity be modified without going through aggregate root?
CHECK: does aggregate enforce all business invariants on every mutation?
ANTI_PATTERN: direct database update bypassing aggregate logic
FIX: all mutations through aggregate methods
BOUNDED_CONTEXTS¶
RULE: same term in different contexts = different types
RULE: translate explicitly at context boundaries
ANTI_PATTERN: sharing domain model across bounded contexts (library reuse)
FIX: separate models, explicit translation at boundary (anticorruption layer)
ANTI_PATTERN: assuming "Customer" means same thing in billing and support
FIX: BillingCustomer ≠ SupportCustomer — translate at boundary
NOTE: syntactically identical code across contexts is NOT DRY violation — different semantics
READ_ALSO: domains/security/microservices-security.md
SECURITY:TAINT_ANALYSIS¶
CONCEPT: track whether data has been validated (untainted) or comes from external source (tainted)
RULE: external input = tainted until validated through domain primitive
RULE: domain primitives = untainting boundary
RULE: never use tainted data in sensitive operations
CHECK: can external input reach SQL/shell/template without passing through domain primitive?
CHECK: is there a clear untainting boundary in the codebase?
PROPAGATION: tainted + untainted = tainted (conservative)
DECISION:WHEN_TO_USE_WHICH_PATTERN¶
IF simple value with invariants
→ DOMAIN PRIMITIVE (Username, Email, OrderId)
IF long-lived mutable object with identity
→ ENTITY with constructor validation
IF entity has >5 state transitions
→ ENTITY STATE OBJECT pattern
IF entity read frequently, mutated rarely
→ ENTITY SNAPSHOT pattern
IF entity lifecycle has distinct phases
→ ENTITY RELAY pattern
IF sensitive value (password, token, key)
→ READ-ONCE OBJECT
IF consistency boundary with child objects
→ AGGREGATE with root access only
IF crossing system/service boundary
→ explicit translation via BOUNDED CONTEXT
WIKI_REF: domains/security/books/secure-by-design.md (full chapter mapping)
READ_ALSO: domains/security/index.md, domains/security/pitfalls.md