DEVBLUEPRINTS

Blog

  • Sobre o blog
  • Arquivo
  • Newsletter
  • RSS

Legal

  • Termos e Privacidade
  • Contato
  • Sobre mim

Inscreva-se na Newsletter

Autorizo o envio de comunicações por e-mail ou qualquer outro meio e concordo com os Termos e Política de Privacidade

 

Blog
  • Sobre o blog
  • Arquivo
  • Newsletter
  • RSS
Legal
  • Termos e Privacidade
  • Contato
  • Sobre mim
© 2026 Todos os direitos reservados — Desenhado e construido comFooter Heartpor Ednaldo Luiz
Blog/System Design

System Design: what it is and how to design systems in practice

Learn System Design with a booking system: requirements, data, capacity, failures and trade-offs, without adding complexity before you need it.

system design
software architecture
scalability
software engineering
System Design: what it is and how to design systems in practice
Ednaldo Luiz
Ednaldo Luiz
Level: Intermediate
Level:
Published: 22 de outubro de 2026
Last updated: 22 de outubro de 2026
14 min read
views: - views

Introduction

A scheduling screen looks simple: show the free slots, let the user pick one and save the booking.

Now two people pick the same slot at the same time. Both get a confirmation. In another attempt, the booking is saved, but the response gets lost on the network. The user tries again. Meanwhile, the email provider slows down and the screen keeps loading.

It's common to associate System Design with giant companies, big tech interviews or systems with millions of users. That makes sense: those are the places where the topic shows up the most.

But this fictional scenario needs none of that to go wrong. Concurrency, a communication failure and decisions that were left implicit are enough. A clinic with three practitioners already has all of it: two patients picking the same slot, a response lost on the network and nobody having decided what happens when the email is late.

Organizing controllers, services and repositories helps you build the code. But we still need to decide who confirms the booking, which data needs protecting and what keeps working when a dependency fails.

That's where System Design comes in.

Let's follow this booking system from the problem to the first design, without starting from a list of technologies.

TL;DR

System Design is designing how components, data and communication work together to meet real requirements.

Start with the expected behavior, the limits and what must not go wrong. Then draw an initial solution, test it against load and failures, and add complexity when there's a concrete reason to.


What System Design is

System Design is the activity of defining responsibilities, interfaces, data and interactions so that a system delivers the expected behavior and quality. The design needs to account for both the features and the requirements for performance, security, availability and evolution.

It isn't just choosing where to put a cache. It's understanding what work that cache would save, how long the data could be stale and what would happen if it went down.

A practical distinction: System Design is the work of connecting need and solution. Software architecture is the structural design and the set of decisions that this work helps produce and review. The terms overlap; the distinction is there to guide the conversation, not to draw a rigid line.

The first question isn't which technology to use. It's what commitment the system needs to keep.


Start with requirements, not technology

Requirements define what the solution must deliver; constraints limit the paths available. Without both, comparing architectures turns into a contest of preferences. Both the design method from the and Microsoft's guidance start from this information to guide decisions.

For our system, let's assume fixed 30-minute slots, registered in advance, with one opening per practitioner in each slot. Users look up and cancel their appointments; managers follow the schedules of the practitioners on the teams they're responsible for.

Payments, recurrence and integrations with external calendars are out of scope for this first version.

Features, quality and constraints

Each kind of requirement asks something different of the design. For scheduling, the first definitions would be these. The numbers are teaching assumptions, not universal recommendations:

KindQuestionIn the booking system
FeaturesWhat needs to happen?Check availability, book, cancel and follow authorized schedules.
QualityUnder which conditions must it work?Answer 95% of queries within 300 ms at the API, under the peak target load.
ConstraintsWhich limits narrow the choices?Four developers, eight weeks and infrastructure the company already runs.

Each row already shapes the design:

  • Follow authorized schedules looks like just a feature, but it brings an access rule the backend will have to check. It comes back just below.
  • 95% of queries within 300 ms can be tested; “it needs to be fast” can't. The target has an operation, a measurement point and a limit, and that's what separates a verifiable requirement from a vague request, the subject of the article on Requirements Engineering. The target load is still missing; it comes out of the capacity estimate in the next section.
  • Four developers and eight weeks take off the table the options that require running many separate pieces. This row will weigh on the first design.

Invariants: what must stay true

An invariant is a property the system must preserve throughout its operations. In our case:

A slot cannot have two active bookings.

This rule holds when two people click at the same time, when a request is repeated and when there are several instances, or running copies, of the application.

Another rule in the example is that being a manager doesn't grant access to every schedule. Access depends on which team that person is allowed to follow.

Before designing the solution, we've already found two responsibilities: protecting the slot's occupancy and checking the scope of access. They can't depend on the screen alone.


Estimate the load before thinking about scale

A capacity estimate is there to guide choices, not to predict the future precisely. What matters first is the order of magnitude, separating normal traffic, peaks and growth.

Suppose 10,000 daily active users, each running twenty availability queries:

Users per day
Queries per user
Seconds in a day
Average
10,000
×
20
÷
86,400
=
~2.3/s
Users per day
10,000
×
Queries per user
20
÷
Seconds in a day
86,400
=
Average
~2.3/s

That doesn't mean three queries per second is enough. Demand doesn't spread across the day: it piles up when the schedule opens. If a thousand of those people arrive within the same five minutes, the same math changes scale:

People at opening
Queries per person
Seconds in the window
Peak
1,000
×
20
÷
300
=
~67/s
People at opening
1,000
×
Queries per person
20
÷
Seconds in the window
300
=
Peak
~67/s

With room for what the estimate can't see, let's set a test target load of 100 queries and 20 booking attempts per second. The 20 attempts cover one request from each of those thousand people in the first minute. It's a peak assumption to validate, not a measurement.

Growth calls for a different calculation: storage. If we have five thousand new bookings per day, at 2 KB of logical data each, that's about 10 MB per day or 3.65 GB per year, using decimal units. Extra history, indexes, replicas and backups haven't been added yet, and the article on storage estimation shows how to add them up.

Even multiplied a few times, that volume fits comfortably in a single relational database. The math doesn't size the database on its own, but it keeps “we need a distributed database” from showing up without a justification.

Where the demand concentrates matters too. The 20 attempts per second could be for twenty different practitioners or all for the same slot: those are different tests, and the request total doesn't tell that part of the story.


From the problem to the first architecture design

The first design should show responsibilities and data paths, not every infrastructure detail. The model organizes these views by zoom level; here, a small logical view is enough to discuss the main choices.

With four developers and eight weeks, I'd start with a modular application and a relational database. Scheduling, Teams and Authorization would be modules with separate responsibilities, not three microservices.

A would handle notifications. The booking and the pending notification would be persisted in the same transaction; sending would happen later.

The design separates what the user has to wait for from the work that can continue in the background:

ApplicationRelational databaseUserAuthorizationSchedulingTeamsBookingPending notificationWorkerEmail providerPOST /appointmentslater, outside the requestin the sametransaction
Scheduling, Teams and Authorization are modules of the same application. The worker processes persisted tasks; it doesn't decide who managed to book.

This proposal avoids adding network communication between parts the same team changes together. Microservices would be an alternative to evaluate if concrete needs for independent deployment, operation or scaling came up. I wouldn't add them just to split up the design.

The relational database is a candidate because the example needs to relate users, teams and bookings and protect transactional changes. Not because “SQL is always better”. The specific choice still depends on what the company runs and on measured behavior.

The interfaces also need to be explicit. For example:

POST/appointments

In this contract, a successful response will mean booking confirmed, not just a request received for later processing:

201Createdapplication/json
{
	"id": "apt_8f21",
	"slot": "H-42",
	"status": "CONFIRMED"
}
201Createdapplication/json
{
	"id": "apt_8f21",
	"slot": "H-42",
	"status": "CONFIRMED"
}

A 202 Accepted would only promise that the request made it into the queue. That distinction shapes what has to happen before responding.


Data, consistency and concurrency

Picking a database doesn't, by itself, settle who can change the data or how to preserve the rules under concurrency. In our design, the Scheduling module controls bookings; Teams keeps the organizational links that authorization relies on. Sharing a database doesn't mean letting any module rewrite any state.

Checking availability isn't booking

Picture Ana and Bruno checking slot H-42. Both get “available”. Ana confirms first, but Bruno is still looking at the earlier response.

If the application just checks and then writes, with no protection against concurrency, there's room for both operations to go through. Being inside a transaction doesn't automatically make every check-then-write flow safe; the and the constraints in use matter.

Without that protection, the race for H-42 ends like this:

Ana checks:H-42 availableBruno checks:H-42 availableAna writesthe bookingBruno writesthe bookingH-42 with twoactive bookings
Both read the slot as free before any write. Each write, on its own, was valid; together, they break the invariant.

For the fixed slots in this example, one solution would be to keep one active occupancy per slot, protected by uniqueness in the database. The transaction groups the changes: the booking and its occupancy are committed together or rolled back. Uniqueness constraints exist precisely to prevent duplicate values in the protected columns.

Both requests go through the same point of protection:

Ana writes theH-42 occupancyBruno writes theH-42 occupancyUniqueness: oneoccupancy per slotAna: bookingconfirmedBruno: 409
An example where Ana manages to book. Arrival order doesn't guarantee priority; what matters is that only one occupancy is confirmed.

Whoever loses the race gets a conflict, not a confirmation that will be silently undone:

409Conflictapplication/json
{
	"code": "SLOT_UNAVAILABLE",
	"message": "This slot was just booked. Please choose another one."
}
409Conflictapplication/json
{
	"code": "SLOT_UNAVAILABLE",
	"message": "This slot was just booked. Please choose another one."
}

On cancellation, the system releases the occupancy and keeps the booking's history. That's part of the model too.

Only for fixed slots

Uniqueness per slot works because each slot is a fixed opening. With free-form intervals, two bookings can overlap without starting at the same instant, and preventing identical start times isn't enough.

Not all data needs the same freshness

The confirmation has to respect the current occupancy. A management summary, on the other hand, could be updated with a few seconds of delay, if the product accepts it. Those are different commitments within the same system.

An availability cache could speed up queries, but it wouldn't replace the protected check at booking time: the copy may be out of date.

Permissions need a different kind of care. The API must check the user, the action and the resource being accessed. Hiding a schedule in the frontend doesn't protect a direct call to the backend. recommends validating authorization on every request.

Notice how the rule “a manager follows their team” turned into a decision about data source, query and access boundary, not just about which button shows up on screen.


Performance and scalability are different problems

Latency is how long an operation takes. Throughput is how much work the system completes per unit of time. Scalability is its ability to handle more load by adding resources while preserving the defined goals. A system can respond quickly with ten users and degrade as concurrency grows.

Scaling vertically means giving more resources to one instance. Scaling horizontally means adding instances. A load balancer spreads requests across them, but that doesn't remove a shared bottleneck in the database. We also need to avoid a request depending on state kept only in the memory of one specific instance.

Other pieces come in depending on the problem observed:

Observed pressureOption to evaluateThe price of the choice
Many repeated readsCacheReduces hits on the source, but requires dealing with stale data.
Queries overload the primary databaseRead replicaSpreads reads, but asynchronous replication can lag.
Peaks of work that can waitQueue and workersSmooth out peaks, but add waiting, reprocessing and monitoring.
Data or writes hit the limits of one nodePartitioning across nodesSpreads load, but complicates queries and operations across partitions.

These mechanisms solve distinct problems, and each one charges its own price.

In our example, I'd check queries, indexes and the amount of data returned before adding another layer. A full history returned on every access calls for a bounded response, possibly paginated, not necessarily more servers.

And a queue doesn't create infinite capacity. If work keeps arriving faster than the consumers process it, the delay grows.

Suppose that, at peak, confirmed bookings generate 20 emails per second and the provider accepts 10:

Emails arriving
Emails sent
Queue growth
20/s
−
10/s
=
10/s
Emails arriving
20/s
−
Emails sent
10/s
=
Queue growth
10/s

Ten minutes of peak leave 6,000 emails waiting. And more workers don't fix it: the limit is the provider, not whoever consumes the queue.


Design for failure and operations too

A design needs to explain what happens when its parts stop responding. Transient failures, timeouts and overload are part of the problem, and poorly controlled retries can make it worse.

In the booking system, the consequence depends on which part failed.

If the database is unavailable, we don't promise a new booking. Without recording the occupancy safely, responding “confirmed” would break the endpoint's contract.

If the email provider fails, the already confirmed booking stays valid. The persisted pending task makes it possible to try sending later. The worker needs to record success or failure, limit attempts and make old pending tasks visible to operations. In this initial design, a task table can play that role without a separate .

If the response is lost after the confirmation, the outcome is uncertain for the client. A timeout doesn't mean nothing happened. To allow safe retries, we can identify the request and recognize new attempts of the same operation, without producing another booking. This property is called idempotency.

With a key sent by the client, a retry stops being a new request:

ClientServerSends the bookingwith key 7f3cStores the bookingand key 7f3cTimeout:no replyRetries with thesame key 7f3c7f3c already exists:returns the bookingReceives the samebookingthe replyis lost
The server stores the key together with the booking. The second attempt finds the key and returns the result of the first.

Uniqueness protects the slot against two bookings; idempotency helps recognize the repetition of the same request. One doesn't replace the other.

How will we know the system is healthy?

The Google SRE book recommends monitoring latency, traffic, errors and . For scheduling, I'd add booking conflicts, the age of pending notifications and the time spent waiting for a database connection.

A rise in latency calls for investigation: slow processing, an external dependency or waiting on a busy resource? Metrics show aggregate behavior; tracing a single request helps locate where it spent its time.

The article on P50, P95 and P99 goes deeper into this reading. Here, it's enough to remember that “the average looks fine” doesn't end the evaluation.

We also need to agree on how long the operation can be unavailable and how much data loss is tolerable in a recovery. Those targets guide redundancy, backups and restore tests; a managed service doesn't remove the need to verify the result.


How to evaluate and evolve the design

An architecture is a proposal to validate, not proof of capacity. For our example, the tests need to confront the design with the commitments it made:

  1. Compete for the same slot. Two concurrent requests can't produce two active bookings.
  2. Exercise the target load. Queries must meet the latency target at 100 reads and 20 booking attempts per second, using representative data.
  3. Cut off dependencies. Email being unavailable must not block bookings; the database going down must not produce false confirmations.
  4. Test access boundaries. A manager can't see another team's schedules just by changing an identifier in the request.

Then come the costs no diagram shows: who gets the alert, restores the database, maintains the contracts and updates the dependencies. Evaluating an architecture has to account for that work, beyond the infrastructure bill.

And today's design has no fixed next step. Each signal calls for a different change:

Modular applicationand relational databaseReportscompete withbookingsNotificationsfallbehindTeams needindependentdeploymentsIsolatereportsRework thenotification flowRevisitboundaries
Each signal opens an investigation, not an automatic change. Without any of them, today's design keeps serving.

Evolution responds to concrete limits and needs, not to a mandatory roadmap of technologies.

I'd record the proposal in a Design Doc and keep the relevant architectural choices in ADRs. For example: why does the booking need immediate confirmation, while the notification can happen later?

That justification matters when someone proposes putting the whole operation on a queue to “gain scale”. The change may be valid, but it alters the promise made to the user and needs to be discussed as such.


Before wrapping up, five questions on requirements, load, concurrency, failures and the promise made to the user:

Question1/5

All the goals below are about availability queries. Which one lets you evaluate whether the design meets it?


Conclusion

The booking system started with a screen, a slot and a button. The design took shape when we defined what that button needed to guarantee.

There was no need to start with microservices, a distributed cache and several queues. What it took was making responsibilities explicit, protecting the booking and separating the confirmation from the work that could wait.

Learning System Design is developing that reasoning: tying each piece to a requirement, recognizing the cost of the choice and knowing what evidence would make the design change.

On the next feature in your project, pick an important operation and follow its path. Who can run it? What state does it change? What happens if it arrives twice or if the response never comes back?

Before adding another box to the diagram, answer those questions. You'll already be doing System Design.

On this page

Share

Recommended

The components, the failures and the safe retries behind this article's design.

Ednaldo Luiz
GitHubLinkedInPortfólio

Ednaldo Luiz

Software Architect and Engineer | Java & AI

Software Engineer focused on architecture and performance. I work with Java/Spring Boot, well-structured SQL, scalable services on AWS, and GenAI solutions with RAG (LangChain + vector databases). I value readable code and well-justified decisions.