Skip to content

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