Skip to content

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 — forces caller to handle empty case

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