Multi-Database Strategy: Choosing the Right Database for Each Access Pattern

A decision framework for polyglot persistence — when to use relational, document, graph, time-series, key-value, and search databases, with real production trade-offs.

#database#architecture#polyglot-persistence#aws
Cover image for the article: Multi-Database Strategy: Choosing the Right Database for Each Access Pattern

"Use the right tool for the job" is easy advice to give and hard to execute well. Every database vendor claims to solve every problem. Your team advocates for what they know. And every additional database adds operational complexity, data synchronization challenges, and cognitive load for developers who now need to understand five query languages instead of one.

After running polyglot persistence architectures across three companies — scaling from 2 databases to 8 — I've developed a framework for when specialization pays for itself and when a general-purpose database with the right indexing strategy is good enough.

The Decision Framework

The wrong question: "What's the best database?" The right question: "What are my access patterns, and at what scale does specialization pay for itself?"

Database Decision Framework

Access Pattern Classification

PatternCharacteristicsOptimal StoreAWS Service
CRUD with joinsStructured, relational, ACIDRelationalAurora PostgreSQL/MySQL
Key-value lookupSingle-key access, ultra-low latencyKey-ValueDynamoDB, ElastiCache
Document flexibilityNested objects, schema evolutionDocumentDocumentDB, DynamoDB
Relationship traversalMulti-hop connections, path findingGraphNeptune
Time-ordered metricsAppend-only, time-range queriesTime-SeriesTimestream
Full-text + facetsSearch, filtering, aggregationSearchOpenSearch
High-write streamingAppend-only, partition-scoped readsWide-ColumnKeyspaces
Analytical aggregationLarge scans, columnar operationsOLAPRedshift, Athena

Real Architecture: E-Commerce Platform (8 Databases)

Our e-commerce platform serves 12M monthly active users with the following database topology:

┌─────────────────────────────────────────────────────────────────┐
│                        Application Layer                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Aurora PostgreSQL ─── Orders, Products, Accounts (ACID)        │
│  DynamoDB ─────────── Shopping Carts, Sessions (key-value)      │
│  ElastiCache Redis ── Product Cache, Rate Limits (ephemeral)    │
│  OpenSearch ────────── Product Search, Faceted Filtering        │
│  Neptune ──────────── Recommendations, Fraud Detection          │
│  Timestream ────────── User Analytics, Performance Metrics      │
│  S3 + Athena ───────── Event Archive, Ad-Hoc Analytics         │
│  Redshift ─────────── Business Intelligence, Reporting          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Why Each Database Was Chosen

// Service-to-database mapping with justification
const databaseDecisions: DatabaseDecision[] = [
  {
    service: 'Order Management',
    database: 'Aurora PostgreSQL',
    justification: 'Multi-table transactions (order + line items + inventory decrement), complex reporting queries, regulatory audit trail requiring ACID',
    alternatives_considered: ['DynamoDB (rejected: cross-item transactions)', 'CockroachDB (rejected: operational overhead)'],
    scale: '2M orders/month, 500 concurrent transactions',
  },
  {
    service: 'Shopping Cart',
    database: 'DynamoDB',
    justification: 'Single-item access by user_id, variable schema (different products have different attributes), 0ms cold start requirement, survives AZ failures',
    alternatives_considered: ['Redis (rejected: durability requirement)', 'PostgreSQL (rejected: overkill for key-value access)'],
    scale: '800K concurrent carts, 50K updates/sec peak',
  },
  {
    service: 'Product Search',
    database: 'OpenSearch',
    justification: 'Full-text search with typo tolerance, faceted filtering (price range, category, brand), relevance scoring, synonym expansion',
    alternatives_considered: ['PostgreSQL FTS (rejected: faceting performance)', 'Algolia (rejected: cost at scale)'],
    scale: '2M searches/day, 200ms P99 requirement',
  },
  {
    service: 'Recommendations',
    database: 'Neptune',
    justification: 'Collaborative filtering via graph traversal (users who bought X also bought Y), fraud ring detection requiring 4-6 hop traversals',
    alternatives_considered: ['PostgreSQL recursive CTE (rejected: 6-hop performance)', 'Custom ML model (supplements, doesn\'t replace)'],
    scale: '50M edges, 12M vertices, real-time queries',
  },
  {
    service: 'User Analytics',
    database: 'Timestream',
    justification: 'Time-series access patterns (DAU/MAU, funnel conversion over time), automatic data lifecycle (hot → cold), native time-binning functions',
    alternatives_considered: ['PostgreSQL with TimescaleDB (rejected: operational overhead at 500M events/day)', 'InfluxDB (rejected: managed service preference)'],
    scale: '500M data points/day, 90-day retention',
  },
];

Cost Reality: Polyglot vs. Monolithic

The operational cost of running 8 databases is real. Here's the honest breakdown:

Cost CategoryMonolithic (PostgreSQL only)Polyglot (8 databases)Delta
Infrastructure$8,500/mo$14,200/mo+$5,700
Engineering (operations)$4,800/mo (26h)$7,200/mo (40h)+$2,400
Engineering (development)Higher per featureLower per feature-$3,200*
Data sync infrastructure$0$1,800/mo+$1,800
Total$13,300/mo$23,200/mo+$9,900

*Development savings estimated from features that would require complex workarounds in a monolithic database (graph queries, full-text search, time-series analytics).

Cost vs Complexity Tradeoff

The polyglot approach costs more. The question is whether the capabilities it enables (real-time recommendations, sub-200ms search, fraud detection) generate more than $9,900/month in business value. For our platform, product recommendations alone drove $240K/month in incremental revenue.

Data Synchronization: The Hidden Complexity

Every additional database needs data. Either it's the source of truth for its domain, or it needs to be synchronized from elsewhere. This creates a synchronization topology that must be designed, monitored, and maintained:

// Data synchronization topology definition
interface SyncTopology {
  source: string;
  target: string;
  mechanism: 'CDC' | 'Event' | 'ETL' | 'Dual-Write';
  latency: string;
  consistency: 'eventual' | 'strong';
  failureMode: string;
}

const syncFlows: SyncTopology[] = [
  {
    source: 'Aurora (Products)',
    target: 'OpenSearch (Search Index)',
    mechanism: 'CDC',
    latency: '<2s',
    consistency: 'eventual',
    failureMode: 'Search returns stale results until sync recovers',
  },
  {
    source: 'Aurora (Orders)',
    target: 'Neptune (Recommendations)',
    mechanism: 'Event',
    latency: '<5s',
    consistency: 'eventual',
    failureMode: 'Recommendations based on slightly stale purchase history',
  },
  {
    source: 'Aurora (Orders)',
    target: 'Timestream (Analytics)',
    mechanism: 'Event',
    latency: '<10s',
    consistency: 'eventual',
    failureMode: 'Dashboard metrics delayed, no business impact',
  },
  {
    source: 'Aurora (Orders)',
    target: 'Redshift (BI)',
    mechanism: 'ETL',
    latency: '1 hour',
    consistency: 'eventual',
    failureMode: 'BI reports show previous hour data',
  },
  {
    source: 'DynamoDB (Carts)',
    target: 'Aurora (Orders)',
    mechanism: 'Event',
    latency: 'At checkout',
    consistency: 'strong',
    failureMode: 'Checkout fails, cart preserved for retry',
  },
];

Synchronization Monitoring Dashboard

We monitor every sync flow with three metrics:

Sync FlowLag SLAAlert ThresholdCurrent Lag
Products → OpenSearch<5s>10s1.8s
Orders → Neptune<10s>30s4.2s
Orders → Timestream<30s>60s8s
Orders → Redshift<2h>4h45min
All flows combinedN/A>2 failures/hour0

When NOT to Use Polyglot Persistence

Polyglot persistence is often premature optimization. Here's when to stay monolithic:

Stay on PostgreSQL if:

  • Your data fits in 500GB
  • You have fewer than 50K queries/second
  • Your team has fewer than 15 engineers
  • You're pre-product-market-fit
  • Your queries are satisfiable with proper indexing

Add a specialized database when:

  • A specific access pattern is degrading despite proper indexing
  • The specialized database enables a feature that's impossible (not just slower) in your primary store
  • You have the engineering capacity to operate and synchronize it
  • The business value clearly exceeds the operational cost
// Decision scoring function
function shouldAddDatabase(proposal: DatabaseProposal): Decision {
  const scores = {
    performanceGap: scorePerformanceGap(proposal), // 0-10
    featureImpossibility: proposal.impossibleInCurrent ? 10 : 0,
    teamCapacity: scoreTeamCapacity(proposal.teamSize, proposal.currentDbCount),
    businessValue: scoreBusinessValue(proposal.expectedRevenue, proposal.monthlyOperationalCost),
    syncComplexity: -scoreSyncComplexity(proposal.requiredSyncs), // Negative = cost
  };

  const total = Object.values(scores).reduce((a, b) => a + b, 0);

  if (total > 20) return { decision: 'ADD', confidence: 'high' };
  if (total > 12) return { decision: 'ADD', confidence: 'medium' };
  if (total > 5) return { decision: 'EVALUATE_FURTHER', confidence: 'low' };
  return { decision: 'STAY_MONOLITHIC', confidence: 'medium' };
}

Operational Playbook: Running 8 Databases

Operational Playbook Overview

Team Organization

We don't have 8 database specialists. Instead:

  • Platform team (2 engineers): Owns infrastructure provisioning, backup verification, security patching, and monitoring for all databases.
  • Domain teams: Own the data model and queries for their database. The team building recommendations owns the Neptune schema and queries.
  • On-call rotation: Single unified rotation. Runbooks cover each database's common failure modes.

Standardized Monitoring

Every database, regardless of type, reports four golden signals:

SignalMetricAlert Threshold
LatencyP99 query time>2x baseline
TrafficQueries/second>80% capacity
ErrorsFailed queries/min>0.1% error rate
SaturationCPU/Memory/Connections>75% utilization

Key Takeaways

  1. Access pattern drives database choice — not technology preference, not vendor marketing, not conference talks.
  2. Every database you add costs $2-4K/month in total operational burden — infrastructure + monitoring + synchronization + cognitive load.
  3. Data synchronization is the real complexity — the databases themselves are well-understood. Keeping them consistent is where bugs live.
  4. Start monolithic, specialize deliberately — PostgreSQL handles 90% of workloads adequately. Add specialized databases when you hit proven limitations at scale.
  5. Business value must exceed operational cost by 3x — anything less, and the complexity isn't worth the marginal performance gain.

Polyglot persistence is a powerful architecture when applied with discipline. The discipline is saying "no" to the sixth database until the fifth one has proven its value and its synchronization is bulletproof.

Comments

    No comments yet. Be the first to share your thoughts.