ADR-025: Production Hardening, Targeted Cache Invalidation, and Enterprise Security Controls¶
- Date: 2026-09-20
- Status: Accepted
- Phase: Phase 9B
Context & Problem Statement¶
Following an end-to-end Principal Engineer architectural and code audit of the Payflow API, multiple production-readiness gaps, latent security exposures, and scalability bottlenecks were identified across security, exception handling, data caching, transactional outbox lifecycle, and testing:
-
Production JWT Secret Fallback Vulnerability (
SEC-01): InJwtTokenProvider, if thePAYFLOW_SECURITY_JWT_SECRETenvironment variable was omitted in a production environment, the application silently fell back to a hardcoded, publicly visible test secret, allowing attackers to forge arbitrary user and admin tokens. -
Broken Object Level Authorization & Balance Enumeration (
SEC-02): EndpointsGET /api/v1/users(bulk user directory) andGET /api/v1/users/balance/{amount}(balance threshold querying) lacked administrative role constraints, permitting any authenticated standard user to harvest customer profiles, phone numbers, and account balances. -
Transport & Frame Security Exposures (
SEC-03,SEC-04): Spring Security configuration allowed credential inclusion (allowCredentials = true) on wildcard origins (allowedOrigins = *), violating the Fetch/CORS specification and opening credential exposure risks. Additionally,frameOptions().disable()disabled clickjacking defenses globally. -
Spring 6.1+ / Spring 7 Parameter Validation Handling (
ARCH-02): Method parameter validation violations (@Min,@Max,@PathVariable,@RequestParam) in Spring Framework 6.1+ / 7 throwHandlerMethodValidationExceptionrather thanMethodArgumentNotValidException, which resulted in generic unhandled 500 errors instead of standardized RFC 9457ProblemDetailresponses. -
Unbounded Idempotency Key Injection (
ARCH-03): TheIdempotency-Keyheader was accepted without length or character constraints, allowing arbitrarily large strings to cause database column overflows (VARCHAR(255)) or resource exhaustion in distributed locks. -
Unbounded Growth in Transactional Outbox Registry (
ARCH-04): Spring Modulith persists completed event publications to theevent_publicationtable. Without an automated maintenance job, this table grows indefinitely under high transfer volumes, degrading query performance and exhausting database storage. -
Cache Stampede via Blanket Eviction (
PERF-01): On every transfer,@CacheEvict(value = {"users", "user_ledgers"}, allEntries = true)wiped all user profiles and ledger histories across the entire application, causing heavy thundering-herd database queries under concurrent traffic. -
Integration Test Execution Blindspot (
TEST-01,TEST-02):pom.xmllackedmaven-failsafe-plugin, causing all containerized integration tests (*IT.javacovering concurrency, deadlock avoidance, and Kafka outbox delivery) to be bypassed duringmvn testand CI builds. Furthermore, repository slice tests (@DataJpaTest) were absent forTransactionRepositoryandIdempotencyRepository.
Considered Options¶
For Cache Eviction:¶
- Option A (Global Invalidation): Continue using
@CacheEvict(allEntries = true). Simple, but causes massive cache stampedes and database churn in production. - Option B (Targeted Invalidation — Chosen): Programmatically evict only keys belonging to the transaction participants (
senderandreceiverbyid,upiId, andreferenceId, plus sender/receiver ledger caches) viaCacheManager. Unrelated user cache entries remain untouched.
For Outbox Cleanup:¶
- Option A (Custom SQL Cron): Run custom native SQL
DELETE FROM event_publication WHERE completion_date < .... Bypasses Spring Modulith abstraction and couples cleanup to vendor-specific table schemas. - Option B (Spring Modulith CompletedEventPublications — Chosen): Inject
CompletedEventPublicationsfromspring-modulith-events-apiand invokedeletePublicationsOlderThan(Duration.ofDays(7))via a scheduled@Transactionaljob at 02:00 AM UTC.
For Production JWT Protection:¶
- Option A (Rely on Deployment Checklist): Hope operators set
PAYFLOW_SECURITY_JWT_SECRET. Fragile and prone to human error. - Option B (Fail-Fast Environment Invariant Check — Chosen): Inject
EnvironmentintoJwtTokenProviderconstructor. Whenenvironment.matchesProfiles("prod"), verify secret is non-null, distinct fromDEFAULT_SECRET, and $\ge$ 256 bits (32 chars); throwIllegalStateExceptionon bootstrap otherwise.
Decision Outcome¶
We selected the production-grade options across all audit dimensions:
- Security:
JwtTokenProvidervalidates secret length and prevents default secret usage inprod.UserControllerenforcesSecurityUtils.hasRole("ADMIN")ongetUsersandgetUsersWithBalanceAbove, throwingForbiddenOperationException(HTTP 403).SecurityConfigenforcesframeOptions().sameOrigin()and disallows credentials whenallowedOriginscontains*.- Error Handling & Input Validation:
GlobalExceptionHandlerhandlesHandlerMethodValidationException,ConstraintViolationException, andMethodArgumentTypeMismatchException, returning RFC 9457ProblemDetailwith 422 and 400 statuses.IdempotencyFiltervalidatesIdempotency-Keylength ($\le 255$) and format (^[A-Za-z0-9_.:-]+$), rejecting invalid keys with 400 Bad Request.- Outbox Maintenance:
OutboxCleanupServiceruns daily at 02:00 AM UTC using Spring Modulith'sCompletedEventPublications.deletePublicationsOlderThan(Duration.ofDays(7)).- Performance:
TransactionService.evictTargetedCaches()selectively invalidates only sender and receiver cache keys (usersanduser_ledgers), preventing cache stampedes for unrelated active users.- Testing & Build Lifecycle:
maven-failsafe-plugin3.5.6 bound tointegration-testandverifyphases with<include>**/*IT.java</include>.- Added
@DataJpaTestslicesTransactionRepositoryTestandIdempotencyRepositoryTest. - Persistence Schema Validation Alignment:
- Extended
V6__create_event_publication_registry.sqlwithevent_publication_archivetable and indexes to satisfy Spring Modulith 2.0'sArchivedJpaEventPublicationentity during Hibernateddl-auto: validate. - Aligned
User.phoneNumberlength constraint (length = 10) andTransactionforeign key nullability (nullable = false) to match Flyway migration DDL definitions.
Consequences¶
Positive¶
- Zero Authentication Bypass: Production deployment cannot start with insecure default JWT secrets.
- Zero BOLA Vulnerability: Directory scraping and balance enumeration are strictly blocked for non-admin tokens.
- Stable Cache Hit Ratio: Transfers no longer wipe cached sessions of unrelated users.
- Bounded Database Growth: Outbox records are automatically pruned after the 7-day retention window.
- Guaranteed Integration Verification: All Testcontainers tests run consistently in standard Maven verify lifecycles.
Negative / Trade-offs¶
- Targeted cache eviction requires explicit coordination of multiple key formats (
upi:,ref:,id:) rather than a single blanket eviction annotation. - Administrative operations require minting admin tokens with
ROLE_ADMINauthority in test environments (@WithMockUser(roles = "ADMIN")).