Built for vibecoders, trusted by enterprises, priced for all 🚀 (Partner)
AI has made it ridiculously easy to go from “I have an idea” to “wait… I actually built this”.
You prompt your coding agent, get the app running, and move on. But as soon as real users, sensitive data, or AI agents enter the picture, auth becomes one of those things you really don’t want to “vibe code” your way through.
That’s why Auth0 is worth a look.
Auth0 has long been a go-to identity platform for enterprises, but it’s now making the economics a lot more interesting for developers and startups, too—including a $0 free plan and new B2B plans designed to make it easier to start small and scale up.
If you’re shipping an app, SaaS product, or AI agent and need authentication without slowing down your build, check out Auth0.
Same Auth0. Better math. Try it for free.
(Thanks to Auth0 for partnering on this post.)
§
Most people think system design is about building the “best” architecture.
It’s not... instead it’s about making the right tradeoffs for a given workload & constraints.
Here are 50 tradeoffs you should understand:
Latency vs throughput. Batching many requests together can reduce network, serialization & processing overhead and increase throughput1. Yet the downside is individual requests wait longer while a batch forms. So use batching when throughput matters and individual requests when low latency matters.
Latency vs tail latency. A system can have excellent median latency while a tiny number of slow requests create poor p95 and/or p99 latency2. Redundant/hedged requests3 can reduce these long tails, but they consume extra capacity. So use hedged requests when low tail latency matters & normal requests when conserving capacity matters.
Compression vs CPU usage. Compressing data reduces network bandwidth & storage requirements. Yet compression and decompression require CPU and could add processing latency4. So use compression when bandwidth & storage matters and uncompressed data when low CPU usage & processing latency matter.
Telemetry fidelity vs observability cost. Recording ALL distributed traces gives you information for debugging production issues. Sampling5 reduces processing and storage costs, but you’d miss occasional traces you later need. So use full tracing when maximum debugging visibility matters & use sampling when observability costs matter.
Cache freshness vs cache hit rate. Longer TTLs increase cache hits6 and reduce requests to the origin. But they also keep stale data cached longer. So use longer TTLs when a high cache hit rate matters & shorter TTLs when data freshness matters.
Local cache vs shared cache. An in-process cache avoids a network request and can provide extremely fast reads. A shared remote cache makes data easier to share across instances, but adds network latency & dependency. So use a local cache when low read latency matters, and a shared cache when many instances need shared cached data.
Database indexes vs cheaper writes. Database indexes7 can boost query performance. But they consume storage & add work when rows get inserted, updated, and/or deleted. So use indexes when query performance matters and keep them low when write performance matters.
High utilization vs capacity headroom. Running infrastructure close to maximum utilization can reduce cost per unit of work. While extra headroom8 allows the system to better absorb traffic bursts & latency spikes. So use high utilization when cost efficiency matters, and extra headroom when handling traffic bursts matters.
§
Share this letter & I’ll send you some rewards for the referrals.
§
Consistency vs availability during network partitions. Replicas cannot coordinate when requests arrive during a partition. Maintaining strong consistency requires “rejecting” some operations, while remaining available replicas can temporarily diverge9. Prioritize consistency when stale/conflicting data is unacceptable, and prioritize availability when serving requests during partitions matters.
Cross-region consistency vs write latency10. Synchronously coordinating data across distant regions provides stronger consistency guarantees. Yet the network round trips between regions increase write and/or transaction latency. So use synchronous cross-region replication when strong consistency matters & asynchronous replication when low write latency matters.
Read quorum vs write quorum. Replicated systems contact many replicas during reads/writes11. This approach provides stronger guarantees, but increases latency & use extra resources. Use a larger read quorum when stronger read guarantees matter, and a larger write quorum when stronger write guarantees matter.
Synchronous vs asynchronous replication. Synchronous replication waits for another replica before acknowledging a write to reduce the risk of losing acknowledged data. Asynchronous replication12 gives faster writes, but replicas can lag behind. So use synchronous replication when protecting acknowledged writes matters, and asynchronous replication when faster writes matter.
Single-region vs multi-region deployment. Keeping data in one region typically lowers write latency13 & has less infrastructure complexity. Multi-region setup improves geographic resilience and brings reads closer to users, but replication becomes expensive & complicated. So use a single region when simplicity and low write latency matter, and multiple regions when geographic resilience matters.
Replica reads vs primary reads. Reading from a nearby replica reduces latency and improves read capacity. Yet if the replica is behind the primary, the application would read stale data14. Use local replica reads when read latency matters, and use primary/strongly consistent reads when data freshness matters.
Serializable vs weaker transaction isolation. Serializable isolation makes concurrent transactions behave as though they executed serially. Stronger isolation causes more transaction aborts & retries under contention, while weaker isolation allows more anomalies15. Use serializable isolation when preventing concurrency anomalies matters, and use weaker isolation when higher concurrency matters16.
Optimistic vs pessimistic concurrency control. Optimistic concurrency allows operations to proceed & check for conflicts later. This works well when conflicts are rare. Pessimistic locking prevents conflicts earlier but can increase blocking17. Use optimistic concurrency when conflicts are rare, and pessimistic concurrency when conflicts are frequent/costly18.
Last-write-wins vs concurrent updates. Last-write-wins automatically resolves conflicting values & keeps conflict handling simple. Version tracking & explicit merging preserve more concurrent updates, but require extra metadata and application logic19. Use last-write-wins when simple conflict resolution matters, and versioning & merging when preserving concurrent updates matters20.
Normalization vs denormalization. Normalized data reduces duplication and makes updates easier to stay consistent. Denormalization makes reads faster and avoids expensive joins, but you must keep duplicate data synchronized21. Use normalization when reducing duplication & consistency matter, and denormalization when faster reads matter22.
Relational databases vs NoSQL. Relational databases23 make it easy to query data in different ways and connect related data across tables. NoSQL databases can scale across many machines for known access patterns, but adding new query patterns can be harder. So use relational databases when flexible queries and relationships matter, and NoSQL when horizontal scaling around known access patterns matters.
B-tree storage vs LSM-style storage. B-trees24 organize data so reads are fast, including looking up one value/range of values. LSM trees optimize for heavy writes by writing data efficiently & reorganizing it later, but reads & background compaction can require extra work. Use B-trees when fast point & range reads matter, and LSM trees25 when high write throughput matters.
Leveled vs universal LSM compaction. LSM databases periodically merge stored files to clean up & reorganize data. Leveled compaction does more rewrites to keep data neatly organized, thus making reads more efficient. Universal compaction does less rewrites, making writes more efficient, but reads require checking more data. So use leveled compaction when read performance matters & universal compaction when write performance matters26.
Range partitioning vs hash partitioning. Range partitioning27 keeps nearby keys together; this is useful for range queries. Hashing spreads writes more evenly across partitions, but sacrifices natural key locality. Use range partitioning when range queries & key locality matter, and hash partitioning when evenly distributing writes matters28.
Replication vs sharding. Replication copies the same data to many nodes, thus improving redundancy & read capacity. Sharding divides “different” data across nodes. This increases aggregate storage & write capacity while making routing & cross-shard operations harder29. Use replication when redundancy & read scaling matter, and sharding when storage & write scaling matter.
Event sourcing vs state-based persistence. Event sourcing stores every change as an event & rebuilds the current state by replaying those events. State-based storage stores the latest state directly, thus making reads and updates simpler, but it does NOT preserve every historical change30. Use event sourcing when audit history & replay matter, and state-based storage when simpler reads & updates matter31.
Vertical vs horizontal scaling. Vertical scaling increases a server’s CPU, memory, and/or storage capacity without distributing the workload across many machines. Horizontal scaling adds more machines & offers better scalability, but requires the system to distribute requests/data across them32. Use vertical scaling when simplicity matters, and horizontal scaling when you need to scale beyond one machine.
Stateful vs stateless compute. Keeping state inside an application server makes operations simpler & faster. Yet stateless servers are easy to replace, rebalance, and autoscale because durable state lives elsewhere33. Use stateful compute to simplify operations, and stateless compute when scaling & replacement matter.
Monolith vs microservices. A monolith keeps calls, transactions, deployments, and debugging inside a smaller operational boundary. Microservices split the application into independent services that can be deployed and scaled separately, but add network calls, distributed data, & operational complexity. Use a monolith when simplicity matters, and microservices when independent deployment & scaling matter34.
Coarse-grained vs fine-grained services. Coarse-grained services handle larger parts of the application, resulting in fewer service boundaries & network calls. Fine-grained services35 handle tiny, focused functions and can be deployed & scaled independently, but require extra network communication & distributed coordination36.
Shared database vs database per service. A shared database makes joins & transactions between services straightforward. Giving each service its own database increases autonomy and independent evolution, but makes cross-service queries and transactions hard37. Use a shared database when cross-service joins and transactions matter, and use a database per service when service autonomy matters38.
Single model vs CQRS. A single model handles both reads & writes, thus keeping the application simpler. CQRS39 separates read & write models so each can be optimized and scaled independently, but adds infrastructure & synchronization between them. Use a single model40 when simplicity matters, and use CQRS when reads & writes need independent optimization/scaling.
Synchronous calls vs asynchronous messaging. Synchronous calls41 provide immediate success/failure and are straightforward for short operations. Queues42 decouple services and absorb traffic bursts, but introduce delayed execution, duplicate handling & workflow-state complexity.
Saga43 choreography vs saga orchestration. With choreography, each service reacts to events and triggers the next step without a central controller. With orchestration, a central orchestrator tells each service what to do and tracks the overall workflow. Use choreography for simpler, loosely coupled workflows, and use orchestration for complex workflows44 that need centralized coordination & recovery.
Two-Phase Commit (2PC) vs Saga. 2PC coordinates transactions across many services/databases so they either ALL commit or ALL roll back. But coordination can increase latency & reduce availability during failures. A Saga45 breaks the workflow into separate local transactions and uses compensating actions when a later step fails. Use 2PC when “strict” atomicity matters46 and Sagas for long-running distributed workflows where services need more autonomy.
Message queue vs event log. A message queue distributes tasks across workers, with each task typically processed by one worker. An event log47 stores events for a retention period, allowing many consumers to read the same events & replay them later. Use message queues48 for distributing background tasks and event logs when replay & many independent consumers matter.
At-least-once vs exactly-once processing. At-least-once processing retries messages after failures, so the same message would be processed more than once. Exactly-once processing prevents the same operation from producing duplicate effects, but needs extra coordination, transactions, and/or deduplication49. Use at-least-once when consumers can safely handle duplicates, and exactly-once50 when prevention of duplicate effects matters.
Total ordering vs partitioned ordering. Total ordering processes all events in the same order, but limits how much work can run in parallel. Partitioned ordering51 guarantees order only for events with the same partition key, allowing different partitions to process events in parallel. Use total ordering when strict ordering across all events matters, and partitioned ordering when high throughput matters52.
Push-based vs pull-based messaging. With push-based messaging, the broker sends messages to consumers as they arrive. With pull-based messaging, consumers request messages from the broker when they are ready53, thus giving them more control over batching & processing rate. Use push when immediate delivery matters, and pull54 when consumers need more control over how fast they process messages.
Batch processing vs stream processing55. Batch processing56 collects data & processes it together at scheduled intervals, thus making it efficient for BIG datasets. Stream processing processes data continuously as it arrives, providing lower-latency results but adds unbounded data, state, time, and fault recovery complexity.
REST vs GraphQL. REST exposes predefined endpoints that return fixed data structures. GraphQL lets clients request exactly the fields they need through a query57, but adds complexity for query execution, caching, & performance control. Use REST for simpler, resource-based APIs, and GraphQL when clients need flexible data access58.
Unary RPC vs streaming RPC. Unary RPC sends one request & receives one response. Streaming RPC keeps the connection open so the client and/or server can continuously send many messages59 but makes load balancing & debugging complex60. Use unary RPC for simple request-response operations, and streaming RPC61 for continuous/real-time communication.
§
§
Synchronous APIs vs asynchronous APIs. A synchronous API waits for the operation to finish before returning a result, but consumes connection & timeout budgets62. An asynchronous API starts the work and returns immediately, usually with an ID you can use to check its status later. Yet it requires explicit state for status, completion & cancellation. Use synchronous APIs for quick operations, and asynchronous APIs for long-running operations63 that take too long for a single request.
Acknowledgment latency vs durability. A messaging system can confirm a write after fewer replicas store it. This reduces latency but increases the risk of losing the message if the server fails. Waiting for more replicas before confirming the write improves durability but increases latency. Use fewer acknowledgments when low latency matters, and more acknowledgments when stronger durability matters64.
Retries vs load amplification. Retries can recover from temporary failures by resending a failed request. But if a service is already overloaded65, retries create even more requests and can worsen the overload. Use retries for temporary failures, and limit retries when a service gets overloaded.
Short vs long timeouts. Short timeouts release resources quickly when dependencies are slow. Yet setting them too aggressively can abandon requests that would have completed successfully a moment later. Use short timeouts when fast failure and resource protection matter, and long timeouts66 when operations legitimately need more time.
Idempotency vs implementation complexity. Idempotency67 makes retries safer because sending the same request many times produces the same intended effect as sending it once. But implementing it requires extra logic to detect & handle duplicate requests. Use idempotency when requests would be retried, and duplicate actions could cause problems. Use simpler non-idempotent operations when duplicates are harmless/impossible.
Aggressive vs conservative circuit breakers. An aggressive circuit breaker stops sending requests to a failing service quickly, protecting it from more traffic but potentially blocking requests during a temporary problem. A conservative circuit breaker68 waits for more failures before stopping requests, reducing unnecessary circuit openings but sending more traffic to a struggling service.
Load shedding vs graceful degradation. Load shedding69 rejects requests when a system is overloaded, thus freeing up capacity to handle the remaining requests successfully. Accepting more requests increases the chance of serving everyone, but during overload it can slow the entire system/cause failures. Use load shedding when system stability matters, and accept more requests when enough capacity is available.
Deep queues vs backpressure. Deep queues70 store more work when producers send data faster than consumers can process it, thus helping to absorb temporary traffic spikes. But if the queue gets too large, requests can sit there for a long time & become stale. Backpressure slows down/rejects producers when consumers cannot keep up, thus preventing the backlog from growing too large. Use deep queues to absorb temporary traffic spikes and backpressure to prevent large backlogs.
Active-active vs active-passive. Active-active runs the application in many regions at the same time, so another region can continue serving traffic if one fails. Active-passive71 keeps a secondary region on standby & switches traffic to it only when the primary region fails. This makes coordination simpler but increases failover time. Use active-active when high availability across regions matters, and active-passive when simpler multi-region operation matters.
Low RTO/RPO vs cost & complexity. A lower Recovery Time Objective72 (RTO) means recovering faster after a failure, while lower Recovery Point Objective (RPO) means losing less data. Achieving both requires more replication, backup infrastructure, automation & testing,,, this increases cost & complexity. Use low RTO/RPO when minimizing downtime & data loss matters, and higher RTO/RPO when reducing cost & complexity matters.
I started by telling you system design is about tradeoffs…
After 50 of them, the pattern is clear: every architecture choice gives you something & costs you something.
The skill is knowing which tradeoff your workload can afford.
“There are no solutions. There are only trade-offs.”
- Thomas Sowell
§
If you are serious about system design, you really can’t miss out on the footnotes about these 50 tradeoffs:
Want to reach 250K+ tech professionals at scale? 📰
If your company wants to reach 250K+ tech professionals, advertise with me.
Thank you for supporting this newsletter.
You are now 250,001+ readers strong, very close to 251k. Let’s try to get 251k readers by 7 September. Consider sharing this letter with your friends and get rewards.
Y’all are the best.






