Learn System Design with Me — Week 5 / 12
APIs & Service Architecture
Overview
In Week 5, I shifted my focus from how systems store and process data to how different clients and services communicate with them.
The main goals this week were to:
- Understand the differences between REST, GraphQL, and gRPC
- Learn how to think about API design as part of system architecture
- Understand API versioning and why it matters
- Design an API that can support multiple clients, such as web and mobile
- Redesign an existing API with scalability and maintainability in mind
One thing that became clear this week is that an API is much more than a collection of endpoints.
It is a contract between systems.
REST vs GraphQL vs gRPC
One of the main topics this week was understanding that there is no universally "best" API technology.
The right choice depends on the communication pattern and requirements of the system.
REST
REST is probably the style I've worked with the most throughout my career.
It is resource-oriented and commonly uses HTTP methods such as:
GETPOSTPUTPATCHDELETE
For example:
GET /users/123
REST is simple, widely supported, and works very well for public-facing APIs.
GraphQL
GraphQL takes a different approach.
Instead of the server defining exactly which fields are returned by each endpoint, the client can request the data it needs.
For example, a mobile application might only need:
{
user {
id
name
avatar
}
}
while a web application might request additional information.
This can be especially useful when different clients have very different data requirements.
However, GraphQL also introduces additional complexity around:
- Query complexity
- Caching
- Authorization
- Monitoring
- Preventing expensive queries
gRPC
gRPC is designed more for service-to-service communication than typical public APIs.
It uses Protocol Buffers and provides strongly typed contracts between services.
For example:
Order Service
↓
gRPC call
↓
Payment Service
The main takeaway for me was:
REST, GraphQL, and gRPC aren't competitors where one simply replaces the others.
They solve different communication problems.
A system can even use multiple approaches at the same time.
For example:
Web / Mobile
↓
REST API
↓
Backend Services
↓
gRPC
↓
Other Internal Services
API Versioning
Another important topic this week was API evolution.
APIs often live much longer than the code that initially created them.
Once clients depend on an API, changing its contract can break those clients.
For example, imagine we initially have:
GET /api/v1/users/123
and the response contains:
{
"id": 123,
"name": "John"
}
Later, we want to make a breaking change to the response.
Instead of immediately breaking existing clients, we can introduce:
GET /api/v2/users/123
This allows older clients to continue using v1 while newer clients migrate to v2.
The important lesson wasn't simply "use /v1 and /v2."
It was understanding that API contracts need to evolve without unnecessarily breaking consumers.
Designing a Multi-Client API
For this week's design exercise, I considered a system used by both:
- Web clients
- Mobile clients
At first this sounds simple.
But the requirements of the clients can be very different.
A web application might have:
- Large screen
- Fast network
- More bandwidth
- More complex UI
A mobile application might have:
- Limited bandwidth
- Higher latency
- Smaller payload requirements
- Unreliable network connections
This creates an important API design question:
Should every client receive exactly the same data?
Not necessarily.
API Design Trade-offs
One approach is to create a single REST API that serves all clients.
┌── Web
│
Client ───────┼── Mobile
│
└── Other clients
↓
REST API
This is simple and easy to maintain initially.
But as clients become more complex, the API can start returning either:
- Too much data
- Too little data
This is one reason technologies such as GraphQL can become attractive for multi-client systems.
The important part is not choosing GraphQL automatically.
It is recognizing the trade-off between simplicity and flexibility.
API as a Contract
One of my biggest takeaways from this week was thinking about APIs as contracts.
When another system consumes your API, it depends on things such as:
- Request format
- Response structure
- Error behavior
- Authentication
- Performance expectations
- Versioning
Changing an API therefore isn't just a code change.
It can become a system-wide change.
This is why API design should consider not only how an endpoint works today, but also how it might evolve in the future.
Redesigning an Existing API
As part of the practical work, I also looked at how an existing API could be redesigned.
Instead of only asking:
"Does this endpoint work?"
I started asking:
- Who consumes this API?
- What data does each client actually need?
- Which operations are read-heavy?
- What happens when the API evolves?
- Which changes would break existing clients?
- Should this communication use REST, GraphQL, or gRPC?
- How should errors be represented?
- How should the API be versioned?
These questions make API design feel much more like a system design problem.
Reflections
Before this week, I mostly thought about APIs from the perspective of implementing endpoints.
This week made me think about APIs from the perspective of communication between systems.
The technology is only one part of the decision.
The more important questions are:
Who is consuming the API?
What are their requirements?
How will the API evolve?
What happens when there are hundreds of clients depending on it?
That shift in perspective was probably the biggest lesson from Week 5.
What’s Next — Week 6
Next week, I'll move deeper into distributed systems and asynchronous communication:
- Message queues
- Event-driven architecture
- Kafka
- Asynchronous processing
- Designing systems that don't need every operation to happen synchronously
The journey continues 🚀
Top comments (0)