Architecture Review: Content Management Platform
A CMS for publishing and serving structured content: articles, documentation, product pages, and localized variants: where read APIs serve 50–100x more traffic than editorial write APIs. PostgreSQL stores the content graph (articles, authors, categories, taxonomy) and workflow state (draft, in-review, scheduled, published). Redis caches published content objects for read APIs. Elasticsearch powers full-text content search with faceting and relevance ranking. Cache invalidation on publish must be fast and complete; N+1 query patterns on content relationship traversal are the dominant database performance risk during reads.
Evidence Confidence
Moderate
strong
Executive Summary
Content Management Platform: moderate operational readiness (83% evidence confidence). 0 architectural strengths identified, 6 operational risks to manage. Primary concern: Cache Stampede (Dog-Pile). Requires Intermediate operational maturity.
Readiness Rationale
Overall moderate readiness across 8 dimensions. Strong: migration, observability, failure recovery.
Key Concerns
- !Cache Stampede (Dog-Pile)
- !Thundering Herd (Cache Stampede)
Key Strengths
- +Architecture is well-defined for the read heavy application problem profile
8
Assessments
2
Tradeoffs
6
Sections
11
Recommendations
Readiness Assessments
8Architectural Tradeoffs
2Recommendations
11Monitor: Thundering Herd (Cache Stampede)
risk_monitoringWhen a popular cached key expires or a service recovers from downtime, all requests that were waiting or arrive simultaneously miss the cache and hit the origin database concurrently, producing a request spike that can overwhelm the database within seconds.
Affects 1 node. (Redis)
Monitor: Cache Stampede (Dog-Pile)
risk_monitoringWhen a widely-shared cached value expires or is invalidated, all concurrent requests that miss simultaneously trigger identical expensive database queries, overwhelming the origin store before any single result can be computed and cached: a positive feedback loop that can collapse the database within seconds.
Affects 1 node. (Read-Heavy API Backend)
Implement: Monitor generic risk probe signals
observabilitySeed 'Thundering Herd (Cache Stampede) Risk Probe' identifies 2 metrics relevant to thundering_herd.
Metrics to instrument: error_rate, p95_latency_ms
PostgreSQL primary serving all content reads directly (no caching) → Redis cache-aside for published content with explicit publish invalidation
migration_planningTrigger: Content read API p99 > 100ms during peak traffic; PostgreSQL read IOPS exceeding 80% of provisioned capacity; read replica falling behind on heavy read workloads. Migrate from 'PostgreSQL primary serving all content reads directly (no caching)' to 'Redis cache-aside for published content with explicit publish invalidation'. Warm the cache on publish by writing the new content value into Redis as part of the publish transaction, not by deleting the key and waiting for the first read to repopulate. This eliminates the miss window entirely.
Cache invalidation logic must be tested against every content state transition in the editorial workflow: a missed invalidation on schedule-to-published transition will serve stale content indefinitely until the TTL expires; Cache population must be synchronous with the publish operation from the editorial team's perspective: if the first reader after publish always gets a cache miss and slow database response, editors will report that "the site is slow after publish"
PostgreSQL full-text search (tsvector, GIN index) → Elasticsearch with incremental indexing via CDC or outbox
migration_planningTrigger: Full-text search p99 > 500ms; inability to implement faceted filtering (filter by category, date range, author simultaneously) with acceptable PostgreSQL query performance; relevance ranking quality insufficient for user-facing search. Migrate from 'PostgreSQL full-text search (tsvector, GIN index)' to 'Elasticsearch with incremental indexing via CDC or outbox'. Use the outbox pattern or PostgreSQL LISTEN/NOTIFY to trigger incremental Elasticsearch indexing on content change, rather than polling. This reduces indexing lag from polling interval to near-real-time and avoids the database overhead of constant polling queries.
Elasticsearch indexing lag creates a window where newly published content is not yet searchable; this window must be communicated to editorial teams as a search latency SLA (e.g., "new content appears in search within 60 seconds"); Elasticsearch schema mapping changes require a full re-index: plan schema evolution strategy before adding facet fields that may need to change
Prepare runbook for: Burst Traffic Cold Cache Stampede
simulation_preparednessSimulation demonstrates critical degradation of redis, postgresql
Without a runbook, recovery from this failure mode will be ad-hoc
Prepare runbook for: Connection Pool Exhaustion with Horizontal User Scale
simulation_preparednessSimulation demonstrates critical degradation of postgresql
Without a runbook, recovery from this failure mode will be ad-hoc
Plan evolution: Single Cache Layer → Distributed Cache
evolution_planningEvolution from Single Redis Node / Sentinel Cluster → Distributed Redis Cluster (Consistent Hash Ring)
Migration complexity: medium. Rollback: complex.
Plan evolution: Direct DB Queries → CQRS Read Models
evolution_planningEvolution from Unified Read/Write Database → CQRS with Separate Read Projections
Migration complexity: high. Rollback: complex.
Monitor threshold: Tier 1: N+1 Query Amplification
scaling_monitoringSignal: PostgreSQL pg_stat_statements showing > 10 distinct query patterns with high call counts from the content list API path; database queries per second growing linearly with API request rate for content list endpoints (should be sub-linear with proper batch fetching); p99 for content list API > 200ms during moderate traffic
Bottleneck: ORM-level N+1 patterns in content relationship traversal: author fetch, category fetch, related content fetch as independent queries per article. Evolution: Audit every content API response with query logging enabled and count queries per request for each content type; implement eager loading for all included relationships (JOIN for 1:1, IN-clause batch for 1:N); validate that content list endpoints produce a fixed number of queries regardless of list size (O(1) queries, not O(N)); add a query count assertion to integration tests for content list endpoints to prevent regression
Monitor threshold: Tier 2: Cache Invalidation Thundering Herd
scaling_monitoringSignal: PostgreSQL read replica CPU spike correlated exactly with publish events; Redis cache hit rate dropping to near 0% immediately after publish for popular content; content API p99 spiking from < 20ms to > 500ms during the 1–3 second window after a high-traffic content item is published
Bottleneck: Cache key deletion on publish triggering simultaneous cache miss stampede for all concurrent readers of popular content. Evolution: Implement cache-aside with probabilistic early expiration (PER): before the cache TTL expires, a fraction of reads proactively refresh the cache value while other reads continue serving the cached value; this eliminates the hard expiry boundary that causes simultaneous misses; alternatively, on publish, write the new content value directly into the cache key before invalidating the old one (update-in-place rather than delete-and-miss) to eliminate the invalidation gap
Scaling Pressure Signals
8PostgreSQL pg_stat_statements showing > 10 distinct query patterns with high call counts from the content list API path; database queries per second growing linearly with API request rate for content list endpoints (should be sub-linear with proper batch fetching); p99 for content list API > 200ms during moderate traffic
Threshold
Tier 1: N+1 Query Amplification
Likely Bottleneck
ORM-level N+1 patterns in content relationship traversal: author fetch, category fetch, related content fetch as independent queries per article
Recommended Evolution
Audit every content API response with query logging enabled and count queries per request for each content type; implement eager loading for all included relationships (JOIN for 1:1, IN-clause batch for 1:N); validate that content list endpoints produce a fixed number of queries regardless of list size (O(1) queries, not O(N)); add a query count assertion to integration tests for content list endpoints to prevent regression
PostgreSQL read replica CPU spike correlated exactly with publish events; Redis cache hit rate dropping to near 0% immediately after publish for popular content; content API p99 spiking from < 20ms to > 500ms during the 1–3 second window after a high-traffic content item is published
Threshold
Tier 2: Cache Invalidation Thundering Herd
Likely Bottleneck
Cache key deletion on publish triggering simultaneous cache miss stampede for all concurrent readers of popular content
Recommended Evolution
Implement cache-aside with probabilistic early expiration (PER): before the cache TTL expires, a fraction of reads proactively refresh the cache value while other reads continue serving the cached value; this eliminates the hard expiry boundary that causes simultaneous misses; alternatively, on publish, write the new content value directly into the cache key before invalidating the old one (update-in-place rather than delete-and-miss) to eliminate the invalidation gap
Elasticsearch indexing queue depth > 10,000 during a scheduled content release event; search results for newly published content not appearing within 30 seconds of publish; Elasticsearch bulk index API returning 429 (too many requests) from the indexing worker
Threshold
Tier 3: Elasticsearch Index Throughput During Bulk Publish
Likely Bottleneck
Indexing worker throughput insufficient for bulk publish events where hundreds of content items are published in a short window
Recommended Evolution
Implement index write buffering in the indexing worker: batch Elasticsearch bulk API calls at 100–500 documents per request instead of indexing one document per publish event; configure Elasticsearch index.refresh_interval to 30 seconds during bulk ingest (extend from default 1 second) and reset to 1 second after ingest completes; use index aliases so a bulk re-index can be built on a new index and alias-swapped atomically without search downtime
Content catalog growing to > 10 locale variants per article; Redis cache memory growing proportional to locale count (one key per content_id per locale); PostgreSQL join queries for translation state machine spanning 5+ tables and running > 50ms on the editorial API
Threshold
Tier 4: Translation Variant Volume
Likely Bottleneck
Redis memory usage and PostgreSQL query complexity scaling linearly with locale count per content item
Recommended Evolution
Evaluate separating the translation store from the primary content store: a lightweight key-value store or PostgreSQL JSONB column for translation content vs. the structured relationship graph for content metadata; for Redis caching, use a single key per locale (content:{locale}:{content_id}) with LRU eviction and size the cache for the top 3 most-served locales, not all locales equally
PostgreSQL pg_stat_statements showing > 10 distinct query patterns with high call counts from the content list API path; database queries per second growing linearly with API request rate for content list endpoints (should be sub-linear with proper batch fetching); p99 for content list API > 200ms during moderate traffic
Threshold
Escalation trigger: ORM-level N+1 patterns in content relationship traversal: author fetch, category fetch, related content fetch as independent queries per article
Likely Bottleneck
Tier 1: N+1 Query Amplification
Recommended Evolution
Monitor: error_rate, p95_latency_ms, replication_lag_seconds
PostgreSQL read replica CPU spike correlated exactly with publish events; Redis cache hit rate dropping to near 0% immediately after publish for popular content; content API p99 spiking from < 20ms to > 500ms during the 1–3 second window after a high-traffic content item is published
Threshold
Escalation trigger: Cache key deletion on publish triggering simultaneous cache miss stampede for all concurrent readers of popular content
Likely Bottleneck
Tier 2: Cache Invalidation Thundering Herd
Recommended Evolution
Monitor: error_rate, p95_latency_ms, replication_lag_seconds
Elasticsearch indexing queue depth > 10,000 during a scheduled content release event; search results for newly published content not appearing within 30 seconds of publish; Elasticsearch bulk index API returning 429 (too many requests) from the indexing worker
Threshold
Escalation trigger: Indexing worker throughput insufficient for bulk publish events where hundreds of content items are published in a short window
Likely Bottleneck
Tier 3: Elasticsearch Index Throughput During Bulk Publish
Recommended Evolution
Monitor: error_rate, p95_latency_ms, replication_lag_seconds
Content catalog growing to > 10 locale variants per article; Redis cache memory growing proportional to locale count (one key per content_id per locale); PostgreSQL join queries for translation state machine spanning 5+ tables and running > 50ms on the editorial API
Threshold
Escalation trigger: Redis memory usage and PostgreSQL query complexity scaling linearly with locale count per content item
Likely Bottleneck
Tier 4: Translation Variant Volume
Recommended Evolution
Monitor: error_rate, p95_latency_ms, replication_lag_seconds
Migration Readiness
12Migration Stages
3PostgreSQL primary serving all content reads directly (no caching) → Redis cache-aside for published content with explicit publish invalidation
infoMigration trigger: Content read API p99 > 100ms during peak traffic; PostgreSQL read IOPS exceeding 80% of provisioned capacity; read replica falling behind on heavy read workloads
PostgreSQL full-text search (tsvector, GIN index) → Elasticsearch with incremental indexing via CDC or outbox
infoMigration trigger: Full-text search p99 > 500ms; inability to implement faceted filtering (filter by category, date range, author simultaneously) with acceptable PostgreSQL query performance; relevance ranking quality insufficient for user-facing search
Single PostgreSQL instance serving reads and writes → Read replica routing with CQRS separation for analytics and search
infoMigration trigger: Month-end content performance reports generating sequential scan queries that compete with live content reads; analytics queries running > 30 seconds on the primary causing write latency spikes
Risks
9Cache invalidation logic must be tested against every conten
warningCache invalidation logic must be tested against every content state transition in the editorial workflow: a missed invalidation on schedule-to-published transition will serve stale content indefinitely until the TTL expires
Cache population must be synchronous with the publish operat
warningCache population must be synchronous with the publish operation from the editorial team's perspective: if the first reader after publish always gets a cache miss and slow database response, editors will report that "the site is slow after publish"
Elasticsearch indexing lag creates a window where newly publ
warningElasticsearch indexing lag creates a window where newly published content is not yet searchable; this window must be communicated to editorial teams as a search latency SLA (e.g., "new content appears in search within 60 seconds")
Elasticsearch schema mapping changes require a full re-index
warningElasticsearch schema mapping changes require a full re-index: plan schema evolution strategy before adding facet fields that may need to change
Read replica replication lag during heavy editorial publish
warningRead replica replication lag during heavy editorial publish periods means analytics queries may see a slightly stale content catalog: acceptable for analytical use cases but must not be used for editorial workflow state (draft vs. published status must always read from primary)
CQRS routing must be explicit per query path: a query that a
warningCQRS routing must be explicit per query path: a query that accidentally routes to the replica for a freshness-sensitive operation causes an editorial correctness issue
Projection lag creates a read-after-write window where users
criticalProjection lag creates a read-after-write window where users see stale data after their own writes. Mitigation: Route immediate post-write reads to the write store (session-scoped write token); accept eventual consistency only for non-user-initiated reads
↗ direct-db-to-cqrs
Projection rebuild after schema change can take hours or day
criticalProjection rebuild after schema change can take hours or days on large datasets. Mitigation: Design blue/green projection deployment: build new projection in parallel before switching traffic; test rebuild time in staging
↗ direct-db-to-cqrs
Missing partition for current time window causes all INSERTs
criticalMissing partition for current time window causes all INSERTs to fail with 'no partition of relation found'. Mitigation: Create partitions 7-30 days in advance; alert when next partition does not exist before its time window opens
↗ postgresql-to-partitioned
Review Sections
6Referenced Intelligence