System Design for Developers: A Beginner’s Guide with a Mobile Angle
System design is the skill of deciding how clients, servers, databases and queues fit together so a product keeps working as users grow. This guide takes you from the basics to a mobile-specific worked example and a repeatable interview framework.
In this article
- What is system design?
- What are the core building blocks of system design?
- What trade-offs should every developer understand?
- What is different about mobile system design?
- Worked example: designing an M-Pesa-style “send money” feature
- What is a good system design interview framework?
- How do you learn system design in 2026?
- Frequently asked questions
What is system design?
System design is deciding how the parts of a software product fit together so that it meets its requirements for scale, reliability, speed and cost. Code answers “how does this function work?”; system design answers “what runs where, what talks to what, and what happens when something breaks?”.
A useful way to see the difference: sorting a list is an algorithm problem. Showing 500,000 customers their M-Pesa statement within two seconds, on a patchy 3G connection, while a nightly report job is also running, is a system design problem. It involves a phone, an API, a database, maybe a cache, and a plan for failure.
When do junior developers need it?
Earlier than most people think. You do not need to design a platform for ten million users in your first year. But you will decide how to paginate a list, whether to cache a response, how often to retry, and where to store data locally. Those are design decisions with consequences. In design discussions, the junior developers who stand out can explain why they chose an approach, not just how they implemented it.
Mid-level interviews usually add a dedicated design round. If you are heading there, pair this guide with our software developer interview guide and keep your data structures fundamentals sharp, because good design depends on knowing what a hash map or a queue costs.
What are the core building blocks of system design?
Most systems are assembled from eight reusable parts: clients, APIs, load balancers, databases, caches, queues, CDNs and object storage. Learn what problem each one solves and you can read almost any architecture diagram.
| Building block | What it solves | Typical example |
|---|---|---|
| Client | Shows the UI and captures user intent; the part you do not control | Android app, iOS app, browser |
| API (server) | Holds business rules and exposes data to clients in a stable contract | REST or GraphQL service |
| Load balancer | Spreads requests across many servers and removes unhealthy ones | Nginx, cloud load balancer |
| Database | Stores data durably and answers queries | PostgreSQL, MySQL, MongoDB |
| Cache | Serves hot data from memory so the database is not hit every time | Redis, in-app memory cache |
| Queue | Moves slow or bursty work out of the request so users do not wait | Message broker for SMS, emails, receipts |
| CDN | Serves static files from a location close to the user | Images, app config, videos |
| Object storage | Cheap, durable storage for large files | Profile photos, PDFs, backups |
SQL or NoSQL?
Choose SQL when your data has relationships and must stay correct (accounts, payments, orders). Choose NoSQL when you need flexible documents, very high write volume or simple lookups by key (chat history, sessions, feeds). SQL databases give you transactions and constraints; most NoSQL stores trade some of that for easier horizontal scaling. For a beginner product, a single well-indexed PostgreSQL database takes you surprisingly far.
What trade-offs should every developer understand?
Every design decision gives something up. The skill is naming what you are trading, not finding a perfect answer, because none exists.
- Consistency vs availability. When a network splits, a system must choose between refusing requests to stay correct or answering with possibly stale data. A bank balance should be correct; a “likes” counter can be slightly stale.
- Latency vs cost. Caches, replicas and CDNs make things faster and cost money and complexity. Spend where users feel it.
- Vertical vs horizontal scaling. Vertical means a bigger machine; it is simple but has a ceiling and a single point of failure. Horizontal means more machines behind a load balancer; it scales further but forces you to think about shared state and data partitioning.
- Simplicity vs flexibility. A monolith is easier to build, test and deploy. Splitting into services helps large teams, and hurts small ones when done too early.
A rule that has served me well: scale the boring way first. Add an index before a cache, a cache before a new database, and more servers before a rewrite.
What is different about mobile system design?
On mobile you do not control the client, the network or the update cycle. That changes your design more than any other factor, and it is the gap most generic system design tutorials skip.
Offline-first, sync and conflict resolution
Offline-first means the app reads and writes a local database (Room on Android, Core Data or SwiftData on iOS) and syncs with the server in the background. The UI never waits on the network. This matters in Kenya, where users move between Wi-Fi, 4G and dead zones in one trip. The hard part is conflicts: two devices edit the same record offline. Your options are last-write-wins (simple, loses data), field-level merging, or keeping a version number and asking the user. Pick per feature; a note can merge, a payment cannot.
Pagination, image caching and push notifications
- Pagination. Never return an unbounded list. Prefer cursor-based pagination (“give me 20 items after this ID”) over page numbers, because new items arriving mid-scroll would otherwise cause duplicates.
- Image caching. Use a memory cache plus a disk cache, request correctly sized images and serve them via a CDN. Libraries such as Coil on Android and the system’s URL cache on iOS handle most of this.
- Push notifications. Treat push as a hint, not a guarantee. Delivery can be delayed or dropped, so the app must still fetch the true state when opened. Never put secrets in the payload.
API versioning and flaky networks
Old app versions live forever. Some users never update, and you cannot recall a build from a phone. So your API must stay backward compatible: add fields, do not rename or remove them; version endpoints (/v2/) when you must break things; and keep a “minimum supported version” check that can show a polite “please update” screen. For flaky networks, set timeouts, retry with exponential backoff and jitter, and make every write safe to repeat. That last point leads straight into the worked example.
For platform specifics, the official Android developer documentation and Apple developer documentation cover background work and storage in depth.
Worked example: designing an M-Pesa-style “send money” feature
Let us design a simplified peer-to-peer payment feature, the kind that lives inside a wallet or mobile money app. We will follow the same steps you would use in an interview.
Step 1 and 2: requirements and estimates
Functional: a user sends money to another user, sees the result, and views history. Non-functional: money must never be lost or duplicated, the API must tolerate retries, and it must work on weak networks. Out of scope for now: fraud scoring, reversals, bill payments.
Estimate with round, stated assumptions. Say 5 million daily active users averaging 2 transactions a day gives 10 million a day. Divide by roughly 100,000 seconds in a day: about 100 transactions per second on average, and perhaps 5 to 10 times that at peak. That is comfortably within reach of a well-tuned relational database, which is a useful conclusion: you do not need exotic technology.
Step 3 and 4: API and data model
One write endpoint and two reads are enough:
POST /v1/transfers (Idempotency-Key header)
GET /v1/transfers/{id}
GET /v1/accounts/{id}/transfers?cursor=...&limit=20
Data model, in a relational database: accounts (id, balance), transfers (id, from, to, amount, status, idempotency_key unique) and a ledger_entries table where every transfer writes a debit row and a credit row. Store amounts as integers in the smallest unit (cents or shillings times 100), never as floating point.
Step 5: idempotency, the heart of payments
Imagine the user taps Send, the server debits the account, and the response is lost in a dead zone. The app retries. Without protection, the customer pays twice. The fix is an idempotency key: the app generates a unique ID once per user intent and sends it with every retry. The server stores it with a unique constraint and returns the original result if it sees it again.
// Kotlin: generate once per tap, reuse on retry
val key = UUID.randomUUID().toString()
suspend fun send(req: TransferRequest): Transfer =
retryWithBackoff(times = 3) {
api.createTransfer(req, idempotencyKey = key)
}
Server side, wrap the debit, credit and key insert in one database transaction. If the insert violates the unique key, fetch and return the existing transfer instead of running it again. This is also how Safaricom’s Daraja callbacks should be handled: the same callback can arrive more than once, so deduplicate on the transaction ID.
Step 6 and 7: scaling and failure modes
- Scaling reads. Cache account summaries and add read replicas for history. Keep the balance-changing path on the primary database.
- Scaling writes. Run several stateless API servers behind a load balancer. If one database ever becomes the limit, partition by account ID.
- Slow side effects. Push the SMS receipt and push notification onto a queue so the transfer response is fast and a failing SMS gateway never blocks a payment.
- Failure modes. Server crashes mid-transfer (the transaction rolls back), a callback arrives twice (idempotency), the network drops after a send (the app polls
GET /transfers/{id}using its key), and a pending status stuck too long (a reconciliation job compares your ledger with the provider’s records).
Notice what we chose: consistency over availability. If the database is unreachable, the honest answer is “try again”, not a guess.
What is a good system design interview framework?
Use the same seven steps every time. A fixed structure stops you freezing, shows the interviewer how you think, and keeps you from diving into details too early.
- Clarify requirements. Ask who the users are, what the core features are, and what is out of scope. Separate functional from non-functional needs.
- Estimate scale. Users, requests per second, storage per year. Round numbers and say your assumptions aloud.
- Define the API. List the main endpoints or events and their inputs and outputs.
- Sketch the data model. Entities, relationships, and which database type and why.
- Draw the high-level design. Client, load balancer, services, database, cache, queue. Walk one request through it end to end.
- Deep dive. Pick one or two risky parts (sync, hot rows, fan-out) and go deeper. The interviewer often steers you here.
- Failure modes and trade-offs. What breaks, how you detect it, what you gave up. Mention monitoring and retries.
For mobile roles, add a client slice at step 5: local storage, sync strategy, caching, and how an old app version behaves. Most candidates forget it, which makes it an easy way to stand out. More on preparing across all rounds is in our interview guide.
How do you learn system design in 2026?
Learn the building blocks, study real examples, then build small things that force you to use them. Reading alone does not stick; you need to make a decision and live with its consequences.
- Weeks 1 to 2: foundations. Understand HTTP, REST, how a database index works, and what a cache and a queue do. Revisit the table above until you can explain each row without notes.
- Weeks 3 to 4: study real designs. Read engineering blogs from companies that publish their architecture, and work through classic problems: URL shortener, chat app, news feed, payment system. Redraw each from memory.
- Weeks 5 to 8: build. Make a small app with a real backend: an offline-first notes app with sync, then add pagination, an image upload to object storage, and a queue for background jobs. Deliberately break it by turning off Wi-Fi mid-request.
- Ongoing: practise aloud. Explain a design to a friend or record yourself. Time-box to 35 to 45 minutes like an interview.
AI assistants such as Claude, Copilot and Cursor are now a useful sparring partner: ask them to critique your design, list failure modes you missed or play the sceptical interviewer. They are a poor substitute for understanding, because you will be the one on call when it breaks. Use them to check your thinking, not replace it.
If you want structure and feedback, our Professional track covers mobile architecture, offline sync and mock interviews, and you can see the details under the tracks. For a wider view of where this skill sits, read mobile app development in 2026.
Frequently asked questions
What is system design?
Do junior developers need to learn system design?
What is mobile system design?
What is a good framework for a system design interview?
How do I learn system design in 2026?
Is SQL or NoSQL better for a payments app?
Already code? Ship on Android and iOS in 12 weeks
The Professional Track: Kotlin + Swift, Compose + SwiftUI, AI-assisted development, weekly DSA and mock interviews. Evenings and weekends, KES 25,000 a month. Next intakes start 9 November 2026 and 11 January 2027. Free to apply.