Skip to main content
SEMastery
Architectureintermediate

The Interview Question That Changed How I Think About System Design

One simple interview question taught me that good system design is not about fancy tools. It is about honest trade-offs, in plain .NET.

11 min readUpdated November 2, 2025

When I was younger, I thought a great answer in a system design interview meant naming many tools. Redis here. Kafka there. Maybe a service mesh. I treated it like a shopping list.

Then one interviewer stopped me with a question that sounded almost too simple. He asked: "Okay. What did you just give up to get that?"

I went quiet. I had no answer. I had been adding things, but I had never thought about the cost of each thing. That one question changed how I think about system design forever. This post is the lesson I wish someone had told me earlier.

A simple everyday story first

Think about a busy tea stall near a railway station in the morning.

The chaiwala has one stove and one kettle. When ten people come, he is fast. When a train arrives and a hundred people come at once, he has a problem. He cannot make a hundred cups one by one fast enough.

He has choices. None of them are free.

  • He can buy a second stove. That costs money and needs more space.
  • He can make a big pot of tea in advance. That is fast to serve, but the tea gets a little old, and he might waste some if the crowd is smaller than expected.
  • He can ask people to take a token and wait. Nobody is angry, but everyone waits a bit longer.

Notice something. Every choice helps one thing and hurts another. Faster service costs more money. Pre-made tea is quick but slightly stale. Tokens keep order but add waiting.

System design is exactly this. You are the chaiwala. Users are the crowd. And the only honest question is: "What did I give up to get this?"

The chaiwala's choices each trade one good thing for another cost.

The question behind the question

When an interviewer asks you to design something, they are not testing if you know the name "Kafka." They are testing if you understand cost.

A trade-off is a choice to make one thing better while accepting that another thing gets worse. Every real system favours some qualities and pays for it somewhere else. Good engineers say this out loud. Weak answers pretend the cost is zero.

Here is the simple truth I missed for years. There is no perfect design. There is only a design that fits this problem, with costs you have chosen on purpose.

How a senior engineer answers

Listen
Find bottleneck
Pick trade-off
Say the cost

Steps

1

Listen

Ask about scale and reads vs writes

2

Find bottleneck

Name where it will break first

3

Pick trade-off

Choose what to optimise

4

Say the cost

State out loud what you gave up

The shape of a strong system design answer.

A real example: a simple URL shortener

Let me show you the difference with a small .NET example. Imagine a service that turns a long link into a short code, like https://x.co/ab12. When someone visits GET /{code}, you send them to the original long URL.

Here is the most basic version. One database. One lookup.

// The simplest possible design. One table, one query per visit.
app.MapGet("/{code}", async (string code, AppDbContext db) =>
{
    var link = await db.Links
        .FirstOrDefaultAsync(l => l.Code == code);
 
    return link is null
        ? Results.NotFound()
        : Results.Redirect(link.LongUrl);
});

This is honestly fine for a small app. It is easy to read. It is easy to fix at 3 a.m. If you have a few thousand visits a day, ship it and go home.

But now the interviewer leans in. "What if it gets a billion redirects a month?"

This is where I used to panic and add ten tools. Now I do something different. I find the bottleneck first.

Step one: name the bottleneck

For a URL shortener, the key fact is that reads are far more common than writes. People create a short link once, but it might be clicked thousands of times. So the read path is the hot path. That is the bottleneck.

When you say this in an interview, the room changes. You sound like someone who has built things, not just read about them.

OperationHow oftenWhat it needs
Create short link (write)RareCorrectness, no rush
Visit short link (read)Very commonLow latency, high scale
Update or deleteVery rareSimple is fine

Once you know reads are the hot path, the next move is obvious. Make reads fast and cheap. A cache fits perfectly here.

// Read path with a cache in front of the database.
// We trade a tiny bit of freshness for a big jump in speed.
app.MapGet("/{code}", async (string code, IDistributedCache cache, AppDbContext db) =>
{
    var cached = await cache.GetStringAsync(code);
    if (cached is not null)
        return Results.Redirect(cached); // fast path, no DB hit
 
    var link = await db.Links.FirstOrDefaultAsync(l => l.Code == code);
    if (link is null)
        return Results.NotFound();
 
    // remember it for next time
    await cache.SetStringAsync(code, link.LongUrl,
        new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
        });
 
    return Results.Redirect(link.LongUrl);
});

Now here comes the interviewer's favourite question. "What did you just give up to get that?"

And now I have an answer. I gave up freshness. If someone edits a link, the old value can stay in the cache for up to an hour. For a URL shortener, that is usually fine. Links rarely change. So this is a good trade. But I had to say it.

A cache turns most reads into a fast path that never touches the database.

The three trade-offs that come up again and again

Over time I noticed the same handful of trade-offs appear in almost every interview. If you understand these four well, you can reason about most systems.

1. Performance vs scalability

Performance is "how fast is one request." Scalability is "how well does it handle more and more requests." These are not the same. A design tuned for raw speed for one user can fall over when a million users arrive. A design built to scale often adds an extra hop, like a queue or a cache, which makes one single request a little slower but lets the whole system grow.

2. Consistency vs availability

This is the famous CAP idea. When parts of your system cannot talk to each other, you must choose. Do you refuse the request to keep data perfectly correct (consistency)? Or do you answer with maybe-slightly-old data so the user is never blocked (availability)? A bank balance leans towards consistency. A "likes" counter leans towards availability.

3. Simplicity vs flexibility

A simple design is fast to build and easy to debug, but it can struggle to grow. A flexible design handles many futures but adds complexity and cost today. Most teams over-build here. They add flexibility for a future that never arrives.

4. Cost vs everything

More servers, more caches, more regions. Every bit of scale and safety costs real money. Sometimes the right engineering answer is "we do not need that yet."

Trade-offYou gainYou pay
Performance to scalabilityHandles huge growthSlightly slower single request
Consistency to availabilityAlways answersData can be a bit stale
Simplicity to flexibilityMany future optionsMore complexity now
Cheap to robustLower billsMore risk under load

Pick the trade-off on purpose

Bank money
Social likes
Internal tool

Steps

1

Bank money

Choose consistency over speed

2

Social likes

Choose availability, stale is okay

3

Internal tool

Choose simplicity, low cost

Match the trade-off to what the system truly needs.

Watch out for tools dressed up as design

Here is a trap I fell into, and many people still do. We confuse tools with design.

For years, the .NET community reached for libraries like MediatR and MassTransit on almost every project, sometimes without asking why. Then in April 2025, the maintainer announced that AutoMapper, MediatR, and MassTransit were moving to commercial licences. Suddenly many teams panicked, as if their architecture lived inside those packages.

It did not. The mediator pattern, which MediatR made popular, is just a small idea: send a request to a handler instead of calling it directly. You can write it yourself in a few lines of plain C#.

// A tiny home-grown mediator. No external library needed.
public interface IHandler<TRequest, TResponse>
{
    Task<TResponse> Handle(TRequest request, CancellationToken ct);
}
 
// Example handler for creating a short link.
public sealed class CreateLinkHandler
    : IHandler<CreateLinkCommand, string>
{
    private readonly AppDbContext _db;
    public CreateLinkHandler(AppDbContext db) => _db = db;
 
    public async Task<string> Handle(CreateLinkCommand cmd, CancellationToken ct)
    {
        var code = ShortCode.New();
        _db.Links.Add(new Link { Code = code, LongUrl = cmd.LongUrl });
        await _db.SaveChangesAsync(ct);
        return code;
    }
}

The lesson is not "never use libraries." Good libraries save time. The lesson is that your design should not depend on one vendor's licence. Boundaries, names, and trade-offs are the real architecture. Tools are just helpers you can swap.

If you do need messaging today and want open-source options, names like Wolverine and Brighter are worth a look. But choose them because of a trade-off you understand, not out of habit.

Putting it together: how the request flows

Let me show the final shape of our little system. Notice that nothing here is fancy. It is a few honest pieces, each chosen for a reason.

The full read and write path of the URL shortener.

When you explain a diagram like this in an interview, narrate the trade-offs as you go. "The cache makes reads fast, but I accept up to one hour of stale data. The database stays the source of truth, so writes are always correct. If the cache goes down, reads still work, just slower." That sentence is worth more than ten tool names.

How to practise this

You do not need a whiteboard or a big company to practise. Take any everyday system and ask the three questions.

  1. What is the hot path? Where will it break first?
  2. What trade-off helps that path most?
  3. What did I give up, and is that okay here?

Try it on a food delivery app. The hot path is probably "show nearby restaurants," which is mostly reads, so a cache helps, and slightly old restaurant lists are usually fine. Try it on a payment system. There, correctness wins, so you lean towards consistency even if it is a little slower. The answers change with the problem. That is the whole point.

Practice loop

Pick a system
Find hot path
Choose trade-off
Name the cost

Steps

1

Pick a system

Any app you use daily

2

Find hot path

Where load is highest

3

Choose trade-off

What to optimise

4

Name the cost

Say what you gave up

A simple habit to build trade-off thinking.

Quick recap

  • System design is not a shopping list of tools. It is a set of honest trade-offs.
  • The question that changed everything for me was simple: "What did you give up to get that?"
  • Always find the bottleneck first. For many read-heavy systems, that is the read path.
  • Caching trades a little freshness for a lot of speed. That is often a good deal, but say it out loud.
  • Know the four big trade-offs: performance vs scalability, consistency vs availability, simplicity vs flexibility, and cost vs everything.
  • Tools are not architecture. MediatR and MassTransit went commercial in 2025, and good design survived that just fine, because the design was never in the library.
  • A simple, well-explained design beats a complex one you cannot defend.

References and further reading

Related Posts