Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 96

System Design Justification

Recommended resources

Hello! Welcome back to our final module, "CI/CD & System Design Interview Practice."

In the last lesson, we laid the groundwork by outlining a high-level architecture for a TinyURL service. We identified the what—the core components like the application server, cache, database, and a key generation service. While that's a great start, a senior-level interview hinges on your ability to defend your choices. Simply stating "I'll use a NoSQL database" is not enough. You must explain why that choice is superior to others, given the specific system requirements.

Today's lesson focuses on precisely that skill. We will address the learning outcome: Justify the choice of data stores, communication patterns, and resilience strategies for a given system design scenario. We'll revisit our TinyURL design and dive into the critical trade-offs that separate a good design from a great one.

1. Justifying Your Choice of Data Store

Choosing a database is one of the most consequential decisions in system design. Your choice impacts scalability, performance, consistency, and operational complexity. The key is to start with the system's requirements, not with a favorite technology.

To structure this discussion, let's first understand the critical questions you should ask before picking any database.

Exploring trade-offs between NoSQL and SQL in system ...

The article 'Exploring trade-offs between NoSQL and SQL in system design interviews' from DesignGurus provides an excellent framework for making this decision. It begins by emphasizing the need to define the context and requirements.

Please read the sections '1. Defining the Context and Requirements', '2. SQL Databases: Strengths and Trade-Offs', and '3. NoSQL Databases: Strengths and Trade-Offs'. Focus on the key questions in the first section and the comparison of strengths and weaknesses between SQL and NoSQL.

As you've just read, the decision process is guided by analyzing trade-offs across several dimensions:

  • Data Structure: Is your data highly structured with fixed relationships (e.g., user profiles, orders, payments), or is it semi-structured/unstructured (e.g., JSON documents, logs, social media posts)?
  • Query Patterns: Do you need complex JOINs and multi-record ACID transactions, or are your queries simple key-based lookups?
  • Scalability: Do you need to handle massive scale (terabytes of data, millions of requests per second)? Will you scale vertically (bigger server) or horizontally (more servers)?
  • Consistency: Is immediate, strong consistency a must-have (e.g., a financial transaction), or is eventual consistency acceptable (e.g., a new post appearing on a social feed)?

Applying to TinyURL

Let's use this framework to choose the primary database for our TinyURL service, which stores the mapping from a short_key to a long_url.

  1. Data Structure: The data model is extremely simple: a key (short_key) maps to a value (long_url). There are no complex relationships.
  2. Query Patterns: The primary query is a GET by short_key. There are no JOINs needed for the core redirection functionality.
  3. Scalability: From our previous lesson's estimates, we expect billions of reads and hundreds of millions of writes. The system is read-heavy and must scale horizontally to handle this traffic.
  4. Consistency: When a user creates a new short URL, it doesn't need to be instantly available worldwide. Eventual consistency is perfectly acceptable.

Justification:
Based on this analysis, a NoSQL Key-Value store (like Amazon DynamoDB or Apache Cassandra) is an excellent choice.

  • Argument: "I would choose a Key-Value NoSQL database like DynamoDB. Our data model is a simple key-value mapping, and our primary access pattern is fetching a value by its key. These databases are designed for massive horizontal scalability and provide extremely low-latency reads, which is critical for our redirection use case. Unlike a traditional SQL database, they scale out easily without the complexities of manual sharding. Given that we can tolerate eventual consistency, we can leverage these benefits for high availability and performance."

What about the Cache?

Our design included a cache (like Redis). The justification is tied directly to the non-functional requirements.

  • Argument: "To achieve our low-latency requirement and reduce the load on our primary database, I'll use a distributed in-memory cache like Redis. Since our system is read-heavy and some URLs will be far more popular than others (a power-law distribution), a cache will provide significant performance gains. We'll use a Cache-Aside strategy: on a read request, we first check Redis. On a cache miss, we query the database and then populate the cache to serve subsequent requests faster."
Test your understanding!

Imagine you're adding an analytics feature to TinyURL that tracks click counts per URL, country, and browser over time. You need to run queries like "Total clicks for URL 'abc-123' in the last month, grouped by country."

Would the Key-Value store we chose for the main mapping be a good fit for this analytics data? Why or why not?

Show answer

No, it would not be a good fit. Key-Value stores are optimized for simple lookups, not for complex analytical queries, aggregations (GROUP BY), and range scans (e.g., "in the last month"). This analytics use case would be better served by a different type of database, such as a columnar database (like Apache Druid or Google BigQuery) or a data warehouse, which are optimized for high-volume writes and complex analytical queries. This illustrates the concept of polyglot persistence, where you use different data stores for different parts of your system, each suited to its specific workload.


2. Justifying Communication Patterns

Services in a microservices architecture need to communicate. Your choice of pattern—synchronous vs. asynchronous—profoundly affects your system's performance, coupling, and resilience.

When to Use REST, gRPC, or Message Queues

The article 'When to Use REST, gRPC, or Message Queues' gives a fantastic breakdown of the three main communication patterns. Let's start with a video that covers similar ground to get a quick overview.

Please read the introduction and the sections on 'REST: The Universal Standard', 'gRPC: High-Performance RPC', and 'Message Queues: Async Event-Driven'. Pay close attention to the 'When to Use' and 'Avoid for' subsections for each pattern.

The resource highlights a fundamental trade-off:

  • Synchronous (REST, gRPC): The client sends a request and blocks until it receives a response. This is simple to reason about but creates temporal coupling—the client and server must both be available at the same time.
  • Asynchronous (Message Queues): The client (producer) sends a message to a broker and moves on. A consumer processes the message later. This decouples the services and improves resilience but adds complexity.

Applying to TinyURL

Let's analyze the communication links in our TinyURL design.

  1. Client ↔️ Application Server (API):

    • Context: This is the public-facing endpoint for users creating and accessing URLs.
    • Justification: "For the external API, I would choose REST over HTTP. It's the de-facto standard for public APIs, supported by all clients and browsers, and is stateless and easy to understand. While gRPC is faster, its lack of native browser support and human-readability makes it less suitable for this public interface. The client needs an immediate response (the short URL or the redirect), so a synchronous pattern is appropriate here."
  2. Application Server → Analytics Service:

    • Context: After a successful redirection, the application server needs to notify an analytics service to log the click.
    • Justification: "This is a perfect use case for asynchronous communication via a message queue like Kafka or AWS SQS. The core redirection flow should not be blocked or slowed down by analytics processing. By publishing a 'URLClicked' event to a queue, the application server can immediately complete the redirect. This decouples the services; if the analytics service is down, the events are safely buffered in the queue, preventing data loss and ensuring the core system remains resilient and fast."
Test your understanding!

Consider the communication between the Application Server and the Key Generation Service (KGS). Which pattern would you choose, and what's your justification?

Show answer

This is a great point for discussion, as there are valid arguments for both.

  • Option 1: Synchronous (e.g., gRPC): "For this internal, performance-sensitive communication, I'd favor gRPC. Since both are backend services under our control, we can leverage gRPC's performance benefits (binary protocol, HTTP/2) for a low-latency, strongly-typed RPC call. The application server needs a key now to create the mapping, so a synchronous call is natural."

  • A More Advanced Alternative: "To further optimize, instead of a synchronous call for every new URL, the application server could request a batch of 1000 pre-generated keys from the KGS and cache them locally in memory. This turns a high-frequency communication path into an infrequent one, dramatically reducing latency and the load on the KGS. The initial call to fetch the batch could still be gRPC." This shows you're thinking about reducing bottlenecks, not just choosing a protocol.


3. Justifying Resilience Strategies

Resilience is about designing a system that can gracefully handle failures. Your choices of data stores and communication patterns are a huge part of this, but there are also specific patterns to consider.

  1. Redundancy and Failover: This is the most basic strategy. By running multiple instances of your application servers, cache nodes, and database replicas behind a load balancer, you ensure that the failure of a single node doesn't bring down the whole system.

  2. Decoupling with Queues: As we just discussed, using message queues is a powerful resilience pattern. It isolates components, so the failure of a non-critical downstream service (like analytics) does not impact the primary user-facing functionality.

  3. The Circuit Breaker Pattern: In synchronous communication, what happens if a downstream service (like the KGS) is slow or failing? A naive client might keep retrying, hammering the failing service and potentially causing a cascading failure.

    • Concept: The Circuit Breaker pattern wraps the network call. It monitors for failures. After a certain number of failures, it "trips" or "opens the circuit" and immediately fails any further calls for a period, without even attempting the network request. This gives the downstream service time to recover.
    • Justification: "To protect our application servers from a failing Key Generation Service, I would implement a Circuit Breaker on the gRPC client. If the KGS becomes unresponsive, the circuit will open, and URL creation requests will fail fast instead of timing out and consuming server resources. This prevents a problem in the KGS from cascading and taking down our main application."

Conclusion

In a senior-level system design interview, every choice is an opportunity to demonstrate your expertise by articulating the "why." Today, we moved beyond just naming components and learned how to defend our architectural decisions with reasoned arguments based on trade-offs.

Key Takeaways:

  • Justification is Requirement-Driven: Always tie your choices back to the specific functional and non-functional requirements of the system (scale, latency, consistency).
  • Data Store Selection: Use the SQL vs. NoSQL framework. Analyze the data model, query patterns, and consistency needs. For our read-heavy, high-scale TinyURL, a NoSQL Key-Value store is a strong fit.
  • Communication Pattern Selection: Use the Sync vs. Async framework. Use REST for public APIs, gRPC for high-performance internal communication, and Message Queues to decouple services and improve resilience.
  • Resilience by Design: Resilience isn't an add-on. It's built into your architecture through redundancy, decoupling (queues), and fault-tolerance patterns like Circuit Breakers.

Preview of the Next Lesson:
This lesson concludes the system design theory portion of our course. We've gone from high-level architecture to detailed justifications. Now, it's time to put all our practical skills to the test. In the next lesson, we begin our final practical module by tackling: "Create a CI/CD pipeline using GitHub Actions to build, test, and package a Spring Boot application." We'll start building the automated foundation to deploy the kinds of systems we've been designing.