DOMAIN:SECURITY:LEGACY_AND_MICROSERVICES¶
OWNER: koen, eric
ALSO_USED_BY: urszula, maxim, aydan
UPDATED: 2026-03-19
SCOPE: code reviews on legacy code, microservice architecture reviews
SOURCE: Secure by Design (Johnsson/Deogun/Sawano, 2019), Ch. 12-14
LEGACY_CODE:SECURITY_SMELLS¶
AMBIGUOUS_PARAMETER_LISTS¶
CHECK: methods with multiple same-type parameters — easy to swap by accident
EXAMPLE: transfer(account1, account2, amount) — which account is source vs destination?
SEVERITY: HIGH — parameter swap is silent bug with security impact (wrong account debited)
| fix approach | when to use |
|---|---|
| direct: rename + reorder | small, local change; few callers |
| discovery: extract domain types | medium effort; best long-term fix |
| new API: wrap with named params | large codebase; backward compat needed |
BEST_FIX: replace primitives with domain types — transfer(from: AccountId, to: AccountId, amount: Money)
NOTE: even two parameters of same type is a smell — three or more is a guaranteed bug source
UNCHECKED_STRINGS_IN_LOGGING¶
CHECK: does logging code accept raw strings from external input?
ANTI_PATTERN: logger.info("User login: " + username) — log injection if username contains newlines
FIX: sanitize log input — strip control characters, limit length
CHECK: can logs leak PII? (emails, IPs, names, session tokens in log statements)
ANTI_PATTERN: logging full request body "for debugging" — may contain passwords, tokens
FIX: log only structured, sanitized fields — never raw input
SEVERITY: HIGH — log injection enables log forging, PII leak is GDPR violation
DEFENSIVE_CODE_CONSTRUCTS¶
CHECK: excessive null checks deep inside business logic — sign of missing domain primitives
ANTI_PATTERN: every method starts with if (x == null) return default — hides bugs
FIX: enforce non-null at boundary (constructor/API) — internal code trusts types
PATTERN: introduce domain primitives + contracts at boundary → remove defensive checks inside
IF legacy code has defensive checks everywhere THEN:
1. introduce domain primitives at system boundary
2. gradually push validation outward
3. remove internal null checks as boundaries enforce validity
OPTIONAL_DATA_TYPES¶
RULE: use Optional/Maybe/nullable types instead of null for optional values
BENEFIT: caller MUST handle absence — can't accidentally dereference null
ANTI_PATTERN: returning null from methods → NullPointerException time bomb
FIX: return Optional
DRY_MISAPPLICATION¶
RULE: DRY is about IDEAS, not TEXT
ANTI_PATTERN: two pieces of code look identical but represent different domain concepts → merged into shared function
FIX: if they change for different reasons, they are NOT duplicates — keep separate
EXAMPLE: billing address validation and shipping address validation look similar but have different business rules
ANTI_PATTERN: shared utility that both security code and business code depend on → change for one breaks other
FIX: separate by concern — DRY within a bounded context, not across
FALSE_NEGATIVE: DRY violation not flagged because code LOOKS different but expresses SAME idea
CHECK: do shared abstractions get frequent changes that break unrelated consumers?
INSUFFICIENT_TESTING¶
CHECK: are there negative tests (testing that bad input is rejected)?
CHECK: are there boundary tests for domain types?
WARNING: no negative tests = no confidence that invalid input is rejected
WARNING: legacy code with only happy-path tests is HIGH RISK for security bugs
FIX: when touching legacy code, add negative tests for the code you modify
INSUFFICIENT_VALIDATION_IN_DOMAIN_TYPES¶
CHECK: do existing domain types actually validate their invariants?
ANTI_PATTERN: class named EmailAddress but constructor accepts any string
FIX: add invariant validation to existing domain types (format, length, character set)
NOTE: partial domain primitives (types with names but no validation) give false confidence
LEGACY_CODE:REMEDIATION_STRATEGY¶
RULE: don't boil the ocean — introduce domain primitives where you touch code
RULE: prioritize system boundaries first — inputs from users, APIs, databases
STEP 1: identify system boundaries (API endpoints, message consumers, DB reads)
STEP 2: introduce domain primitives at boundaries
STEP 3: push validation outward — new code uses domain types
STEP 4: as you touch legacy code, replace raw types with domain primitives
STEP 5: remove defensive checks as boundaries enforce validity
MICROSERVICES:SECURITY¶
BOUNDED_CONTEXT_AS_SERVICE¶
RULE: each microservice = one bounded context
RULE: translate explicitly at service boundary — never share domain models
ANTI_PATTERN: shared domain library across services → tight coupling + single point of security failure
FIX: each service owns its domain model — translate at API boundary
CHECK: is there a shared domain library? If yes, should it be split?
API_DESIGN_SECURITY¶
RULE: API is the security boundary — all input validation happens here
RULE: use domain primitives for API request/response types
RULE: never expose internal domain model via API — use DTOs/contracts
ANTI_PATTERN: accepting raw JSON and passing to internal logic without validation
FIX: validate + transform to domain types at API entry point
CHECK: does every API endpoint validate input before processing?
CHECK: are API response types different from internal domain types?
MONOLITH_SPLITTING_RISKS¶
CHECK: when splitting monolith, does the split cross a security boundary?
ANTI_PATTERN: extracting service but keeping shared database → implicit coupling, no boundary validation
FIX: each service owns its data — communicate via API only
ANTI_PATTERN: internal method call becomes network call without adding error handling
FIX: add circuit breaker, timeout, retry with backoff for every new network boundary
NOTE: distributed monolith (services tightly coupled via shared DB or sync calls) = worst of both worlds
MICROSERVICES_LOGGING¶
RULE: each service logs to central stream — not local files
RULE: correlation ID (trace ID) on EVERY request — propagate across all services
RULE: semantic versioning in log entries — allows schema evolution
| CIA-T concern | logging requirement |
|---|---|
| confidentiality | domain-oriented logger API — prevent PII from reaching logs by design |
| integrity | log aggregation must be tamper-evident — checksums on batches |
| availability | centralized log service with redundancy |
| traceability | correlation ID across all services — identify full request path |
DOMAIN_ORIENTED_LOGGER_API¶
PATTERN: instead of generic logger.info(string), create domain-specific log methods
EXAMPLE: logger.userLoggedIn(userId) — logger knows which fields are safe to log
BENEFIT: PII can't accidentally reach logs — logger method controls what gets logged
BENEFIT: structured logs by default — no string concatenation
ANTI_PATTERN: logger.info("User " + user.toString() + " logged in") — leaks all user fields
FIX: logger.userLoggedIn(user.id) — only logs safe identifier
SENSITIVE_DATA_ACROSS_SERVICES¶
RULE: CIA-T applies at every service boundary — not just external edge
CHECK: is PII transmitted between services? Is it encrypted in transit?
CHECK: can Service A access Service B's sensitive data it doesn't need?
NOTE: "sensitive" is broad — includes pricing data, business rules, user behavior, not just PII
RULE: principle of least privilege across services — each service gets only the data it needs
SECURITY:CODE_REVIEW_CHECKLIST¶
SOURCE: Secure by Design Ch. 14
SECURITY_FOCUSED_REVIEW¶
CHECK: are domain primitives used for external input? (not raw strings/numbers)
CHECK: are entities consistent on creation? (no two-phase init)
CHECK: are invariants enforced? (constructor validation, no invalid state possible)
CHECK: is mutable state protected? (no public setters, defensive copies)
CHECK: are exceptions handled securely? (no internal details to users)
CHECK: are feature toggles tested in both states?
CHECK: is configuration explicit? (no reliance on framework defaults)
CHECK: are negative tests present? (bad input rejected, not just good input accepted)
CHECK: is logging safe? (no PII, no raw user input, structured format)
CHECK: are external calls protected? (timeouts, circuit breakers)
SECURITY_INCIDENT_RESPONSE¶
RULE: incident handling (restore service) ≠ problem resolution (find root cause)
RULE: incident handling involves WHOLE team, not just "security person"
RULE: every incident → retrospective → process improvement → systemic learning
STANDARD: approach inspired by aviation safety — structured learning reduced air fatality rate 1000x
WIKI_REF: domains/security/books/secure-by-design.md (full chapter mapping)
READ_ALSO: domains/security/secure-design-patterns.md, domains/security/index.md