Cloud and Managed Database Services Questions
Running databases as managed cloud services rather than self-hosting them: choosing and provisioning managed relational and NoSQL offerings (for example Amazon RDS/Aurora/DynamoDB, Azure SQL Database/Cosmos DB, Google Cloud SQL/Spanner), sizing instances and storage, and designing an architecture (read replicas, connection proxies and pooling for managed or serverless compute) to meet a latency or availability target. It covers comparing provisioned versus serverless/autoscaling pricing and operational models, forecasting capacity and cost, and migrating a database onto or between managed deployments: moving a self-hosted database to a managed service, switching a workload from provisioned to serverless (or back), changing a single-AZ deployment to multi-AZ, and the questions to ask before adopting a new managed vendor. The core question is the split of responsibility and cost between the cloud provider and the engineering team: what the provider takes on (patching, infrastructure-level HA, automated backups) versus what remains the team's job (configuration, capacity and cost tuning, security posture like encryption and key rotation). It does not cover backup and restore mechanics, RPO/RTO planning, replication or sharding internals, or SQL-level query diagnostics.
Explain connection pooling and why it matters for managed databases, especially with serverless compute (AWS Lambda) and managed MySQL/Postgres. Name pooling solutions you'd consider, explain how you'd size the pool, and describe what you'd configure to avoid connection storms.
Sample Answer
Connection pooling reuses a small set of already-open database connections across many requests instead of opening a fresh connection (a TCP handshake plus authentication) for every request. It matters most acutely with serverless compute like AWS Lambda because each concurrent Lambda execution environment behaves like an independent short-lived process: if your code opens a database connection on invocation, a burst of concurrent invocations opens that many connections nearly simultaneously, and a managed MySQL or PostgreSQL instance has a hard max_connections ceiling tied to its instance size. A traffic spike that is entirely reasonable in query volume can still take the database down purely on connection count.
Why it matters beyond Lambda
Every open connection costs the database real memory and, for PostgreSQL specifically, a whole backend OS process, so connections are expensive per-unit even when idle. Opening and closing them constantly (rather than reusing a pool) adds latency and CPU overhead from repeated authentication and TLS negotiation on every request, on top of the risk of simply running out of connection slots.
Pooling solutions to consider
- RDS Proxy: AWS-managed, sits between the application and RDS (Amazon's managed relational database service for engines like MySQL and PostgreSQL) or Aurora (AWS's own MySQL- and PostgreSQL-compatible managed database with a different underlying storage engine). It pools and multiplexes connections, supports IAM authentication and credentials via AWS Secrets Manager, and automatically fails over to a standby while preserving application connections. It can only be attached to a writer instance, not a read replica, and it requires the proxy and the database to be in the same virtual private cloud (VPC).
- PgBouncer: self-run, the standard external pooler for PostgreSQL, used constantly in front of Lambda-to-Postgres workloads because of its transaction-mode pooling.
- ProxySQL: the MySQL equivalent, adding query routing and caching on top of pooling.
- Cloud SQL Auth Proxy / connectors: Google Cloud's equivalent pattern for Cloud SQL, handling authenticated, pooled connections from serverless callers.
Sizing the pool
Size the pool from actual concurrency, not guesswork, using Little's Law: the average number of concurrent database connections a workload needs equals its request rate multiplied by the average time each request holds a connection.
L=λ×Wrequests_per_sec = 2000 # peak invocation rate hitting the database
avg_query_duration_s = 0.015 # 15ms average time from "acquire connection" to "commit/release"
concurrent_db_connections_needed = requests_per_sec * avg_query_duration_s
db_max_connections = 1000 # example ceiling for a large Aurora instance class
usable_max_connections = db_max_connections - 50 # reserve headroom for admin/replication
headroom_multiplier = 3 # slack for variance and short bursts above the mean
recommended_pool_size = concurrent_db_connections_needed * headroom_multiplier
print(f"Concurrent DB connections needed on average: {concurrent_db_connections_needed:.1f}")
print(f"Recommended pool size with {headroom_multiplier}x headroom: {recommended_pool_size:.0f}")
print(f"That is {recommended_pool_size/usable_max_connections:.1%} of usable max_connections")
Concurrent DB connections needed on average: 30.0
Recommended pool size with 3x headroom: 90
That is 9.5% of usable max_connections
The point this makes concrete: even at 2,000 requests/sec, if each request only holds a connection for 15ms, the database only ever needs about 30 connections open at once on average, 90 with generous headroom. A pooler lets a huge number of Lambda-side clients share that small, stable set of database-side connections, which is the entire reason pooling and Lambda go together.
What to configure to avoid connection storms
- Use transaction-mode pooling, not session mode. In transaction mode, the database-side connection is returned to the pool the instant a transaction commits, not held for the life of the client's connection to the pooler. This is what lets 30 to 90 real database connections serve thousands of Lambda invocations: each invocation borrows a connection for milliseconds, not for its entire lifetime.
- Point Lambda only at the pooler's endpoint, never at the database engine directly. If any code path bypasses the pooler, it reintroduces the exact one-connection-per-invocation problem pooling exists to solve.
- Set the pooler's client-facing limit high but its database-facing limit bounded near the database's real capacity: thousands of client-side slots, tens to low hundreds of database-side ones.
- Set idle and statement timeouts so an abandoned or slow Lambda-side connection cannot hold a pooled database slot indefinitely.
- Avoid patterns that force "pinning." RDS Proxy (and poolers generally) fall back to a dedicated, unshared connection for a session that changes connection-level state, for example a session-level
SETstatement, an unclosed transaction, or, on RDS Proxy specifically, any statement larger than 16 KB. A pinned session stops multiplexing entirely for its lifetime, which quietly reintroduces the connection-storm risk the pooler was meant to prevent.
Trade-offs and pitfalls
- A pooler is another moving part with its own failure modes: if it goes down, so does every connection through it, so it needs to be highly available in its own right (RDS Proxy handles this for you; a self-run PgBouncer needs its own redundancy plan).
- Transaction-mode pooling breaks session-level features that assume a stable connection: prepared statements that persist across queries, session-level temp tables, and
LISTEN/NOTIFYin Postgres either don't work or behave differently. Audit the application for these before switching modes. - Pooling fixes connection exhaustion; it does not fix a database that is genuinely CPU- or IO-bound. If queries are slow, a pool just queues requests instead of rejecting them outright, which can turn a fast failure into a slow, harder-to-diagnose one if queue depth isn't monitored.
You're designing the managed-database architecture for an ad-serving platform with 100M monthly active users. Characteristics: 95% reads, 5% writes; p95 read latency target < 50ms globally; peak reads 50k RPS; writes are small updates ~10k TPS during peak. Data shape: user preferences and campaign state, schema somewhat flexible. Recommend a managed database architecture (products and components) to meet the latency SLA and discuss caching, indexing, and operational concerns.
Sample Answer
This is a key-value access pattern (look up preferences and campaign state by user or campaign ID) at extreme read scale with a hard global latency target, so the recommendation is DynamoDB as the system of record, fronted by DynamoDB Accelerator (DAX, an in-memory read cache purpose-built for DynamoDB) in each region, replicated across regions with DynamoDB global tables, behind latency-based routing that sends each user to their nearest region.
flowchart LR
Client[Ad-serving client] --> Router[Latency-based routing]
Router --> AppUS[App tier, us-east-1]
Router --> AppEU[App tier, eu-west-1]
AppUS -->|read, cache-first| DAXUS[DAX cluster, us-east-1]
AppEU -->|read, cache-first| DAXEU[DAX cluster, eu-west-1]
DAXUS -->|cache miss| DDBUS[(DynamoDB table, us-east-1)]
DAXEU -->|cache miss| DDBEU[(DynamoDB table, eu-west-1)]
DDBUS <-.async global replication.-> DDBEU
Why this architecture fits the workload
- 95% reads, flexible schema: DynamoDB's item model (no fixed columns per row) fits user preference and campaign-state documents that vary in shape without requiring a schema migration every time a new attribute is added, and it is purpose-built for very high, uniformly-distributed read throughput.
- p95 read latency (the response time that 95% of requests come in at or under; the slowest 5% are allowed to exceed it) under 50ms globally: a single-region database cannot hit this for users far from it; global tables replicate the same table to multiple regions so every region has a local, complete copy, and DAX gives single-digit-millisecond to microsecond reads for cached keys on top of that.
- Writes are small, targeted updates: DynamoDB's per-item write model (not full-row rewrites, not multi-table joins) matches "update this user's preferences" or "update this campaign's remaining budget" well, and 5% writes at 10k TPS peak is comfortably within what a well-partitioned table handles.
Capacity math for the stated peak load
Assume a 1 KB average item size (a compact preference or campaign-state document) and a 90% DAX cache hit ratio for reads, both reasonable for a keyed lookup workload with a lot of repeat access to the same active users and campaigns. DynamoDB prices reads in read capacity units (RCU): a strongly consistent read of an item up to 4KB costs 1 RCU, but an eventually consistent read of the same item costs half that, 0.5 RCU, because DynamoDB can answer it from any replica without first confirming it has the very latest write. A cached preference or campaign-state read here does not need to reflect a write from the last few hundred milliseconds, so eventually consistent reads are the right, cheaper default; strongly consistent reads would double the RCU figures below:
peak_read_rps = 50_000
peak_write_tps = 10_000
dax_cache_hit_ratio = 0.90
RCU_PER_PARTITION = 3000 # AWS-documented per-partition ceiling (read units/sec)
WCU_PER_PARTITION = 1000 # AWS-documented per-partition ceiling (write units/sec)
rcu_per_read = 0.5 # eventually consistent read, item fits in one 4KB chunk
wcu_per_write = 1 # 1KB item fits in one 1KB chunk
backend_reads_per_sec = peak_read_rps * (1 - dax_cache_hit_ratio)
backend_rcu_per_sec = backend_reads_per_sec * rcu_per_read
worst_case_rcu_per_sec = peak_read_rps * rcu_per_read # DAX cold, e.g. right after a deploy
write_wcu_per_sec = peak_write_tps * wcu_per_write
print(f"Backend reads/sec with warm DAX: {backend_reads_per_sec:,.0f}")
print(f"Backend RCU/sec with warm DAX: {backend_rcu_per_sec:,.0f}")
print(f"RCU/sec if DAX is cold: {worst_case_rcu_per_sec:,.0f}")
print(f"WCU/sec (writes, uncacheable): {write_wcu_per_sec:,.0f}")
print(f"Min partitions from writes: {-(-write_wcu_per_sec // WCU_PER_PARTITION):.0f}")
print(f"Min partitions from cold-cache reads: {-(-worst_case_rcu_per_sec // RCU_PER_PARTITION):.0f}")
Backend reads/sec with warm DAX: 5,000
Backend RCU/sec with warm DAX: 2,500
RCU/sec if DAX is cold: 25,000
WCU/sec (writes, uncacheable): 10,000
Min partitions from writes: 10
Min partitions from cold-cache reads: 9
With a warm DAX cache, the DynamoDB table itself only has to absorb roughly 2,500 RCU/sec (well under one partition's ceiling), meaning the table's real sizing constraint here is the write path, which needs throughput spread across at least 10 partitions (10,000 WCU/sec divided by the documented 1,000 WCU-per-partition ceiling). On-demand capacity mode is the right choice at this scale rather than manually provisioned throughput, since the traffic pattern in an ad-serving platform (campaign launches, dayparting) is not perfectly steady, and on-demand removes the need to hand-tune provisioned throughput and auto scaling targets across two regions.
Caching, indexing, and operational concerns
- Partition key design: key user preference items by a well-distributed attribute like
user_id(100M distinct values spreads load evenly). Do not key campaign-state items bycampaign_idalone if a small number of high-spend campaigns dominate traffic; that concentrates writes on a few partitions and creates a hot partition even though the table's aggregate throughput looks fine. Use write sharding (a suffixed key likecampaign_id#shard_n) for any campaign whose write rate alone could approach the 1,000 WCU-per-partition ceiling. - DAX: run a DAX cluster with at least 3 nodes across availability zones (AZs) per region for high availability, and set a write-through or short time-to-live (TTL, how long a cached value is trusted before being refetched) policy on campaign-state items specifically, since a stale campaign budget or targeting rule served from cache for too long has real business cost (overspend, mistargeting), unlike a slightly stale user preference.
- Global tables and consistency: replication between regions is asynchronous and uses last-writer-wins conflict resolution, so a user who is rerouted between regions (failover, or a mobile client crossing a region boundary) can briefly see a slightly older version of their own last write. This is acceptable for preferences and most campaign state, but if a specific field (for example remaining campaign budget) must never double-spend across regions, that field needs a different pattern, for example routing all writes for a given campaign to a single "home" region rather than treating every region as equally writable.
- Monitoring: alarm on
ThrottledRequestsandConsumedWriteCapacityUnitsper table (and per partition-level hot-key indicators), DAX cache hit rate (a real hit ratio far below the assumed 90% means the 2,500 RCU/sec sizing estimate above is wrong and the table needs to absorb more direct traffic), and cross-region replication latency for global tables. - Indexing beyond the primary key: add global secondary indexes (GSIs) only for query patterns that genuinely need them (for example looking up all campaigns for an advertiser), since every GSI is itself a separately-partitioned, separately-throughput-provisioned structure with its own hot-key risk, not a free index the way a relational database's secondary index is.
Compare provisioned capacity (reserved throughput) with serverless/autoscaling models for DynamoDB and Aurora Serverless v2. Discuss cost predictability, latency characteristics, cold-start behavior, throttling, operational overhead, and what kind of workload pattern each model suits best.
Sample Answer
Provisioned capacity means you reserve a fixed amount of throughput (how many read or write operations, or how much compute, the system can handle per second) up front and pay for it whether you use it or not; serverless/autoscaling means the service adjusts capacity to match load and you pay closer to actual consumption. The two managed products that expose this choice most directly are DynamoDB (provisioned throughput vs. on-demand) and Aurora (a fixed instance class vs. Serverless v2), and the right choice depends almost entirely on how predictable and how bursty the workload is.
DynamoDB: provisioned throughput vs. on-demand
| Provisioned | On-demand | |
|---|---|---|
| Cost predictability | High: you set read capacity units (RCU) and write capacity units (WCU) and pay a flat rate for them | Lower: billed per request, cost tracks traffic directly, which is predictable only if traffic is predictable |
| Latency | Same single-digit-millisecond latency as on-demand; capacity mode does not change per-request latency | Same latency profile as provisioned |
| Throttling behavior | Requests beyond provisioned capacity are throttled (ProvisionedThroughputExceededException) unless application auto scaling is configured, and auto scaling reacts to a CloudWatch alarm on a delay, not instantly | Scales automatically to serve the traffic, but a sudden spike well beyond the table's recent peak can still see brief throttling until DynamoDB provisions more partitions behind the scenes |
| Operational overhead | You own setting and tuning target utilization for auto scaling, and re-tuning it as traffic patterns shift | Effectively none: no capacity numbers to manage |
| Best workload pattern | Steady, forecastable traffic where the flat provisioned rate beats per-request pricing | Spiky, new, or hard-to-forecast traffic, or a small table where operational simplicity matters more than shaving cost |
Aurora: provisioned instance vs. Aurora Serverless v2
| Provisioned | Serverless v2 | |
|---|---|---|
| Cost predictability | High: fixed instance-class price regardless of load | Variable: billed in Aurora capacity unit (ACU) hours, each ACU is roughly 2 gibibytes (GiB) of memory plus corresponding CPU and networking, measured every second |
| Latency characteristics | Constant, matches the chosen instance class | Same engine, same query latency at a given capacity; latency only changes if the workload outgrows the current ACU level faster than it can scale up |
| Cold-start behavior | None: capacity is always provisioned | None for the normal 0.5 to 256 ACU scaling range, since scaling happens in place (typically the same host) without dropping connections; a cold-start-like delay only appears if you opt into "scale to zero" (available on newer engine versions with a minimum of 0 ACUs) and the cluster has to resume from a fully paused state |
| Throttling | N/A: fixed capacity either handles the load or the instance is CPU/memory saturated | Scales up automatically in increments as small as 0.5 ACU as load increases; a very sudden spike can still see brief elevated latency while capacity catches up, but there is no hard throttling wall the way DynamoDB provisioned mode has |
| Operational overhead | Manual: resizing the instance class is a deliberate, brief-outage operation you schedule | Low: you set a minimum and maximum ACU range once and the service scales within it continuously |
| Best workload pattern | Steady-state OLTP (online transaction processing) load where you can predict peak capacity and want the most predictable bill | Spiky or unpredictable workloads (promotions, seasonal traffic, dev/test environments that sit idle most of the day) where paying for idle provisioned capacity would be wasteful |
Worked example: turning a workload into numbers
DynamoDB. Say a product-catalog table needs to sustain a peak of 800 strongly consistent reads/sec (items averaging 3 KB) and 150 writes/sec (items averaging 1.5 KB). DynamoDB charges 1 RCU (read capacity unit) per strongly consistent read of an item up to 4 KB, and 1 WCU (write capacity unit) per write of an item up to 1 KB, rounding each item up to the next chunk boundary:
import math
peak_reads_per_sec = 800
peak_writes_per_sec = 150
read_item_kb = 3
write_item_kb = 1.5
rcu_per_read = math.ceil(read_item_kb / 4) # 3KB fits in one 4KB chunk -> 1 RCU
wcu_per_write = math.ceil(write_item_kb / 1) # 1.5KB needs two 1KB chunks -> 2 WCU
read_rcu_per_sec = peak_reads_per_sec * rcu_per_read
write_wcu_per_sec = peak_writes_per_sec * wcu_per_write
headroom = 1.2 # 20% buffer above measured peak
provisioned_rcu = math.ceil(read_rcu_per_sec * headroom)
provisioned_wcu = math.ceil(write_wcu_per_sec * headroom)
print(f"Peak RCU/sec needed: {read_rcu_per_sec}")
print(f"Peak WCU/sec needed: {write_wcu_per_sec}")
print(f"Provisioned RCU to reserve (20% headroom): {provisioned_rcu}")
print(f"Provisioned WCU to reserve (20% headroom): {provisioned_wcu}")
Peak RCU/sec needed: 800
Peak WCU/sec needed: 300
Provisioned RCU to reserve (20% headroom): 960
Provisioned WCU to reserve (20% headroom): 360
Under provisioned capacity, you reserve and pay for 960 RCU and 360 WCU every hour of the day, including overnight when actual traffic might fall to 50 reads/sec and 10 writes/sec (50 RCU and 20 WCU, under 6% of what is provisioned). Under on-demand, you are billed only for the RCU and WCU each request actually consumes, so that overnight trough costs proportionally less, at a higher per-unit rate for whichever units you do consume. This is the concrete version of the "idle capacity" trade-off in the table above: provisioned wins when the 800/150 peak is close to the round-the-clock average, on-demand wins when it is not.
Aurora Serverless v2. Say a workload idles near the 1 ACU minimum for 20 hours a day and spikes to 8 ACU for a 4-hour business-hours peak. An Aurora capacity unit (ACU) is billed per second, in ACU-hours:
idle_hours, idle_acu = 20, 1
peak_hours, peak_acu = 4, 8
daily_acu_hours = idle_hours * idle_acu + peak_hours * peak_acu
rate_per_acu_hour = 0.12 # AWS's published Aurora Serverless v2 rate, us-east-1 at time of writing; check current pricing for your region
daily_cost = daily_acu_hours * rate_per_acu_hour
monthly_cost = daily_cost * 30
provisioned_daily_acu_hours = 24 * peak_acu # a fixed instance sized for the peak, running all day
provisioned_monthly_cost = provisioned_daily_acu_hours * rate_per_acu_hour * 30
print(f"Serverless v2 ACU-hours/day: {daily_acu_hours}")
print(f"Serverless v2 monthly compute cost: ${monthly_cost:,.2f}")
print(f"Fixed-at-peak monthly compute cost: ${provisioned_monthly_cost:,.2f}")
Serverless v2 ACU-hours/day: 52
Serverless v2 monthly compute cost: $187.20
Fixed-at-peak monthly compute cost: $691.20
For this bursty, mostly-idle shape, Serverless v2 costs about 73% less than a provisioned instance sized to never fall short at the 4-hour daily peak, because the provisioned instance bills for 8-ACU-equivalent capacity for all 24 hours whether the workload needs it or not. A workload that instead sat near 8 ACU around the clock would flip this: Serverless v2 would bill close to the same 192 ACU-hours/day as the provisioned option, at a higher per-ACU-hour rate, making the fixed instance the cheaper choice.
Trade-offs and pitfalls
- Provisioned capacity is not "safer," it is differently risky: undersized provisioned throughput throttles real user traffic with a hard error, while serverless/on-demand's failure mode is a cost spike or brief latency bump, not a hard rejection. Which failure mode is worse depends on whether the business cares more about a predictable bill or never dropping a request.
- On-demand and Serverless v2 both have a per-unit price premium over well-utilized provisioned capacity. The trade only pays for itself when the workload's peak-to-average ratio is high enough that you would otherwise be paying for a lot of idle provisioned headroom; for a genuinely flat, 24/7 workload, provisioned is usually cheaper.
- Aurora Serverless v2's scale-to-zero option is attractive for dev/test but is a different feature from its normal 0.5 to 256 ACU scaling: only enable it where an occasional resume delay is acceptable, not on anything customer-facing.
- Mixing modes inside one system is a legitimate design, not a compromise: for example, a steady OLTP writer on a provisioned Aurora instance with Serverless v2 read replicas that scale independently to absorb bursty reporting traffic, or a DynamoDB table on provisioned capacity for its steady core traffic with on-demand reserved for a table that only exists to catch spiky, unpredictable event ingestion.
You are evaluating three workloads and must recommend either a managed relational or a managed NoSQL/data service. Workloads:
A) Payment processing with complex joins, strong transactional integrity, and strict consistency.
B) User profile store with flexible schema, extremely high read scale, and fast reads (p95 < 50ms).
C) IoT telemetry ingestion with massive write throughput, time-series data, and eventual consistency acceptable.
For each workload recommend a managed product (e.g., RDS PostgreSQL, Aurora, DynamoDB, Cloud Spanner), justify your choice, and note the operational considerations you'd need to handle for the chosen service.
Sample Answer
These three workloads sit at genuinely different points on the relational-versus-NoSQL spectrum, and the right call is: managed relational (Aurora PostgreSQL, AWS's own PostgreSQL-compatible managed database) for payments, DynamoDB (AWS's managed NoSQL key-value and document database) for the profile store, and DynamoDB for telemetry ingestion, but for different reasons in each case, since "high write throughput" and "high read throughput" are solved by the same product through different design choices.
Workload A: payment processing, complex joins, strict consistency
Recommendation: Amazon Aurora PostgreSQL (Multi-AZ). Complex joins and strong transactional integrity are exactly what a relational engine with full ACID (atomicity, consistency, isolation, durability) transaction support is built for: multi-table updates (debit one account, credit another, write a ledger row) either all commit or none do, and foreign keys enforce referential integrity the database itself checks. Aurora over plain RDS mainly for its storage-layer replication (faster, more resilient failover (automatically switching to a healthy standby when the primary fails) than RDS's engine-level replication) and higher I/O ceiling if transaction volume grows.
Operational considerations: enable Multi-AZ so a failover does not lose committed transactions; put RDS Proxy in front of it since payment services tend to run many short transactions from many application instances, and connection exhaustion is a common self-inflicted outage; enable encryption at rest and enforce TLS (rds.force_ssl) since this is regulated financial data; test schema migrations against lock behavior specifically, since an ALTER TABLE that takes an exclusive lock on a hot payments table during business hours is a self-inflicted outage separate from any database limitation; enable audit logging for compliance (PCI DSS, payment card industry data security standard) and test point-in-time recovery, not just confirm backups exist.
Workload B: user profile store, flexible schema, extreme read scale, p95 < 50ms
Recommendation: DynamoDB, with DAX (DynamoDB Accelerator, an in-memory read cache) in front. A profile store is a keyed lookup (get this user's profile) with a schema that varies per user and grows new attributes over time without a migration, which is DynamoDB's core strength, and its single-digit-millisecond baseline latency, extended to sub-millisecond with DAX, comfortably clears a 50ms p95 target.
Operational considerations: partition key on user_id for even distribution; add global secondary indexes (GSIs) sparingly and only for real query patterns (for example lookup by email for login), since each GSI is its own separately-throughput-provisioned structure; choose on-demand capacity mode unless traffic is genuinely steady and predictable enough that provisioned throughput with auto scaling is worth the operational tuning; monitor DAX cache hit rate, since a low hit rate means the read load is really landing on the table and the latency budget assumption is wrong.
Workload C: IoT telemetry ingestion, massive write throughput, time-series, eventual consistency acceptable
Recommendation: DynamoDB, partitioned by device_id with a sort key of timestamp (or a time-bucketed composite like device_id#hour), with time-to-live (TTL) enabled to auto-expire old telemetry rather than manually deleting it. Massive write throughput with eventual consistency acceptable is a bad fit for a relational engine's transactional write path but a good fit for DynamoDB's partition-parallel write model, where throughput scales by adding partitions rather than by scaling up a single writer.
Operational considerations: watch for a hot partition on any single very chatty device or on a shared time bucket if many devices write to the same minute-level key, and shard the key (a suffix like device_id#shard_n) if a single device's write rate alone could approach the per-partition write ceiling; use on-demand capacity mode given IoT fleets are rarely perfectly predictable (device rollouts, firmware updates causing retry storms); if the dominant read pattern later turns out to be time-range aggregation across many devices (dashboards, anomaly detection) rather than per-device lookups, stream changes out via DynamoDB Streams into a purpose-built time-series or analytics store (for example Amazon Timestream, or S3 plus a query engine) rather than trying to make DynamoDB itself do wide time-range scans, which it is not optimized for.
Worked example: why the same product needs different partition-key designs
# Workload B (profile store): key by the entity you look up directly
PK = user_id # e.g. "user#48213" -> one item, fast point read
# Workload C (telemetry): key to spread write load across time AND devices
PK = f"{device_id}#{hour_bucket}" # e.g. "sensor-9921#2026-09-24T14" -> bounds any
SK = timestamp # single partition's write rate to one device-hour
Both are DynamoDB tables, but workload B's key exists to make a single, specific item cheap to find, while workload C's key exists to prevent any one device or time window from concentrating write throughput onto a single partition. Recommending "DynamoDB" for both workloads is only half the decision; the partition key design is the other half, and getting it wrong reproduces the exact hot-partition and throttling problems a relational engine would not have had in the first place.
Trade-offs and pitfalls
- Recommending DynamoDB for both B and C is not "the same answer twice": the schemas, partition-key strategies, and even the capacity mode choice differ, and a candidate who says "DynamoDB" without describing how the key design differs has not actually answered the question.
- Aurora PostgreSQL for payments is the safer default, but if this system needed to scale writes globally with strong consistency across regions, Cloud Spanner would be the named alternative worth mentioning: it gives horizontal write scale with external consistency (a stronger guarantee than most distributed databases provide: transactions are ordered by real wall-clock time across every region, so if one transaction finishes in real time before another starts, every reader sees them in that same order, not just eventually), at the cost of a different query and schema model (interleaved tables, where a child table's rows are physically stored next to their parent row so the relationship is declared up front rather than expressed only through foreign keys and joins; no traditional single-writer failover story either) and meaningfully higher operational and cost complexity than a single-region Aurora cluster. For a workload described only as "payment processing with complex joins," that additional complexity is not yet justified.
- Do not pick DynamoDB for workload A just because it is "the managed NoSQL option and the question offers a choice." Strict consistency across multiple related writes (the actual requirement stated) is not what DynamoDB's item-level atomicity model is built for; DynamoDB transactions exist but are more limited and more expensive per operation than a relational engine's native transaction support, and multi-table joins have no equivalent at all.
Your analytics workloads run on a cloud-managed database with autoscaling. Costs have spiked due to heavy ad-hoc queries. Propose concrete optimizations at schema, query, and operational levels to reduce cloud spending while maintaining acceptable performance for analysts. Include cost-vs-latency trade-offs.
Sample Answer
On an autoscaling, consumption-billed managed database, a cost spike from ad-hoc analytical queries is almost always a data-scanned problem, not a raw-traffic-volume problem: the fix has to shrink how much data each query actually touches (schema), stop the worst query shapes from running unbounded (query level), and put a governor between "anyone can run anything" and the bill (operational level), rather than just buying more capacity to absorb the same wasteful queries faster.
Schema-level optimizations
- Partition large fact tables by date (or whatever the dominant filter predicate actually is). A query that only needs the last 30 days should only ever touch the last 30 days of storage; on an engine that meters by data scanned or by compute-seconds spent scanning, this is the single biggest lever available.
- Materialize the handful of aggregation shapes analysts hit repeatedly (daily rollups by region, common group-bys) as precomputed tables refreshed on a schedule, so a repeated "what were yesterday's numbers by region" question hits a small precomputed table instead of re-scanning the raw fact table every time it is asked.
- Add targeted indexes for the columns analysts actually filter and aggregate on, but only where a query pattern justifies it: more indexes also cost money on a consumption-billed system, in storage and in write-time maintenance, so this is a targeted fix for the queries that dominate spend, not a blanket policy.
Query-level optimizations
- Require a date (partition) predicate on ad-hoc queries against partitioned tables. An unbounded query on a partitioned table with no date filter defeats the entire point of partitioning and still scans everything.
- Route common questions to the materialized rollups, through documentation, a curated view layer, or a BI tool's semantic layer that maps a friendly filter to the precomputed structure, instead of leaving every analyst to hand-write the same raw-table aggregation from scratch.
- Cache query results where the underlying data does not change intra-day, so the same or a similar dashboard query, run repeatedly by multiple analysts across a day, pays the real compute cost once rather than once per view.
- Cap runaway queries with a timeout and, where the engine supports it, a bytes-scanned or compute-seconds limit per query, so a single badly written ad-hoc query cannot silently consume a large chunk of the monthly bill before anyone notices.
Operational-level optimizations
- Move genuinely exploratory, ad-hoc analytics off the same autoscaling path as production-critical traffic, onto a separate analytical path sized and billed independently (for example a dedicated read replica (a read-only copy of the database that serves queries without touching the primary) or reporting instance, or exporting to object storage (cheap, durable cloud file storage such as Amazon S3, not a queryable database in itself) and querying it with a serverless query engine). This means one runaway analyst query cannot compound the same bill as the operational workload, and its cost becomes a visible, attributable line item rather than baked into one opaque total.
- Set per-user or per-workgroup cost budgets and alerts, so spend is caught early rather than discovered at month-end.
- Tag and attribute cost by team, dashboard, or query pattern, so "which analysis is actually expensive" is visible and can be prioritized for optimization, rather than the whole database bill being a single number nobody can decompose.
Worked example: what partitioning and a rollup actually save
total_table_size_tb = 10.0
total_days_of_history = 5 * 365 # 5 years retained
typical_analyst_window_days = 30 # a representative "last 30 days" ad-hoc query
unpartitioned_scan_tb = total_table_size_tb
partitioned_scan_tb = total_table_size_tb * (typical_analyst_window_days / total_days_of_history)
print(f"Unpartitioned scan per query: {unpartitioned_scan_tb:.2f} TB")
print(f"Partitioned + date-filtered scan per query: {partitioned_scan_tb:.3f} TB")
print(f"Reduction: {unpartitioned_scan_tb/partitioned_scan_tb:.0f}x less data scanned")
queries_per_day = 50 # the same 30-day question, asked repeatedly across dashboards/analysts
without_rollup_tb_per_day = partitioned_scan_tb * queries_per_day
with_rollup_tb_per_day = partitioned_scan_tb # pay the scan once, in the refresh job
print(f"\n{queries_per_day} repeat queries/day without a rollup: {without_rollup_tb_per_day:.1f} TB/day scanned")
print(f"Same queries against a daily rollup: {with_rollup_tb_per_day:.3f} TB/day scanned")
print(f"Additional reduction from the rollup: {without_rollup_tb_per_day/with_rollup_tb_per_day:.0f}x")
Unpartitioned scan per query: 10.00 TB
Partitioned + date-filtered scan per query: 0.164 TB
Reduction: 61x less data scanned
50 repeat queries/day without a rollup: 8.2 TB/day scanned
Same queries against a daily rollup: 0.164 TB/day scanned
Additional reduction from the rollup: 50x
Partitioning and a mandatory date filter alone cut data scanned by 61x for a representative 30-day-window query against 5 years of history; adding a rollup for the specific shape that gets asked 50 times a day cuts it by another 50x on top of that. Whatever the engine's actual price-per-TB-scanned is, a combined roughly 3,000x reduction in data scanned for the dominant query pattern is where the real savings come from, not from buying more compute to run the same wasteful scans faster.
Cost-versus-latency trade-offs
- Rollups and caching trade freshness for cost and speed: a dashboard reading a daily rollup is only as current as its last refresh, which is the right trade for "yesterday's numbers by region" and the wrong trade for "what's happening right now."
- A hard per-query scan cap protects the budget but will cut off some legitimate deep-dive queries; pair it with an approval or override path for analysts who genuinely need a larger one-off query, rather than a blanket ceiling with no escape hatch, or the optimization will just get worked around informally.
- Requiring a date filter on partitioned tables is a small convenience cost for analysts (they can no longer query "everything, unbounded" in one line) in exchange for the largest single cost reduction available; this is close to a pure win and worth making close to mandatory.
- Moving ad-hoc analytics to a separate path adds a small data-freshness lag (data has to land in the separate store before it's queryable there) in exchange for cost isolation and a predictable, attributable analytics bill instead of one shared with production traffic.
Unlock Full Question Bank
Get access to all 20 Cloud and Managed Database Services interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.