software engineering(Updated September 27, 2026)

Designing Offline-First Sync Engines: Surviving Network Drops in Harare

What system design is, why it matters, and how to think about building software that scales - explained simply for developers ready to move beyond single-file projects.

V

Vincent Mugondora

software engineer & AI Builder

At some point, every developer hits the same wall. You can build features. You can write functions. But when someone asks you to design a whole system - “How would you build an app that handles 10,000 users?” - you freeze.

That’s where system design comes in. It’s the skill that separates developers who build features from developers who architect products.

I’ve designed systems for production applications, taught system design to developers, and learned many of these lessons the hard way. Here’s what I wish someone had explained to me early on.

This article is part of my complete software engineering guide for beginners.

What is system design?

System design is the process of defining the architecture, components, and interactions of a software system to satisfy specific requirements.

In simpler terms: it’s deciding how the pieces of your application fit together before you write the code.

When you build a to-do app, you don’t need system design. When you build a marketplace that handles payments, user accounts, real-time notifications, and file uploads across multiple services - you absolutely do.

Why system design matters

Without it:

With it:

The core concepts

1. Client and server

The most fundamental split in software:

Client - what the user interacts with (browser, mobile app, desktop app)

Server - what processes requests and stores data (your backend)

Every web application has this split. When you type a URL, your browser (client) sends a request to a server, which responds with HTML, JSON, or other data.

Key decision: How much logic lives in the client vs. the server? A “thick client” (like a React app) handles lots of logic locally. A “thin client” (like a server-rendered page) delegates most logic to the server.

2. APIs

An API (Application Programming Interface) is how different parts of your system talk to each other.

REST API - The most common pattern. Uses HTTP verbs (GET, POST, PUT, DELETE) and URLs to represent resources.

GET /api/users/123        → Get user 123
POST /api/orders          → Create a new order
PUT /api/users/123        → Update user 123
DELETE /api/orders/456    → Delete order 456

GraphQL - A query language that lets the client request exactly the data it needs. Good when different clients need different data shapes.

WebSockets - Persistent connections for real-time data (chat, live updates, notifications).

Key decision: Which API style fits your use case? REST is the default for most applications. GraphQL helps when you have complex data needs. WebSockets for anything real-time.

3. Databases

Where your data lives permanently. The two main categories:

Relational (SQL) - PostgreSQL, MySQL. Data in tables with rows and columns. Relationships between tables. Strong consistency (data is always accurate). Best for structured data with clear relationships.

Non-relational (NoSQL) - MongoDB, Redis, DynamoDB. Flexible data shapes. Better for certain patterns (documents, key-value pairs, graphs). Often faster for specific access patterns.

Key decision: Most applications should start with PostgreSQL. It handles 90% of use cases well. Only reach for NoSQL when you have a specific reason (very high write throughput, unstructured data, caching).

4. Caching

Storing frequently-accessed data in a fast layer so you don’t hit the database every time.

Example: Your homepage shows the 10 latest blog posts. Instead of querying the database on every page load, cache the result for 5 minutes. 1,000 requests hit the cache; 1 request hits the database.

Where caching happens:

Key decision: What to cache, for how long, and how to invalidate (refresh) the cache when data changes. Cache invalidation is genuinely one of the hardest problems in software.

5. Load balancing

When one server can’t handle all the traffic, you add more servers. A load balancer distributes incoming requests across them.

Users → Load Balancer → Server 1
                      → Server 2
                      → Server 3

Key decision: When do you need this? For most applications, not until you’re handling thousands of concurrent requests. But designing your app to be “stateless” (no server stores user session data locally) means you can add load balancing later without rewriting.

6. Horizontal vs. vertical scaling

Vertical scaling - Make your server bigger (more RAM, faster CPU). Simple but has limits.

Horizontal scaling - Add more servers. More complex but practically unlimited.

Key decision: Start vertical (it’s simpler). Design for horizontal (so you can switch when needed). This means: don’t store session data on the server, don’t write to local files you expect to persist, and keep services stateless where possible.

Thinking about trade-offs

System design is fundamentally about trade-offs. There’s no perfect architecture - only architectures that optimise for different things.

Consistency vs. availability

Consistency - Every user sees the same data at the same time.

Availability - The system always responds, even if the data might be slightly stale.

Example: A bank account balance must be consistent (you can’t show two different balances). A social media feed can be slightly stale (it’s fine if a like takes 2 seconds to appear for other users).

Simplicity vs. scalability

Simpler architecture - Fewer components, easier to understand, faster to build. But might not handle growth.

Scalable architecture - More components, more complexity, handles growth. But slower to build and harder to debug.

The right choice: Start simple. Add complexity only when you have evidence that you need it. Many successful applications run on a single server for years.

Speed vs. accuracy

Fast responses - Cache aggressively, accept slightly stale data.

Accurate responses - Always read from the source, accept slower responses.

Example: A product catalog can be cached (prices change infrequently). A real-time bidding system cannot (every millisecond matters).

Common architectural patterns

Monolith

Everything in one codebase, one deployment unit.

[Frontend + Backend + Database] → One Server

Pros: Simple to develop, deploy, and debug. One thing to monitor.

Cons: Can become unwieldy as it grows. Scaling means scaling everything together.

When to use: Almost always to start. Don’t split into microservices until you have a team large enough to justify it.

Microservices

Application split into small, independent services that communicate over APIs.

[User Service] ↔ [Order Service] ↔ [Payment Service] ↔ [Notification Service]

Pros: Independent scaling, independent deployment, technology flexibility per service.

Cons: Network complexity, distributed debugging, data consistency challenges, operational overhead.

When to use: When you have multiple teams working on different parts of the system, or when specific components need to scale independently.

Event-driven

Services communicate by publishing and subscribing to events rather than direct API calls.

Order Created → [Event Queue] → Payment Service processes payment
                              → Email Service sends confirmation
                              → Inventory Service updates stock

Pros: Loose coupling, easy to add new consumers, resilient to individual service failures.

Cons: Harder to trace the flow of a request, eventual consistency, more complex debugging.

When to use: When multiple services need to react to the same events, or when you need resilience against individual component failures.

A practical example: designing a marketplace

Let’s walk through how you’d think about designing a simple marketplace (like a service where people buy and sell products).

Step 1: Clarify requirements

Before designing anything, understand:

Step 2: Identify core components

For a marketplace, you need:

Step 3: Choose your architecture

For a team of 1-3 developers building an MVP:

Start with a monolith. One backend (Node.js or Django), one database (PostgreSQL), one frontend (React or Next.js).

[Next.js Frontend]
        ↓
[API Layer (Node.js/Django)]
        ↓
[PostgreSQL Database]
        ↓
[Cloudflare CDN for static assets]

Step 4: Design the data model

What tables do you need?

Step 5: Identify scaling points

Where will problems appear first as you grow?

Step 6: Plan for what you don’t build now

Document decisions and their boundaries:

This is system design in practice. You’re not predicting the future - you’re making decisions explicitly so they can be changed deliberately.

How to get better at system design

1. Study real systems

Read engineering blogs from companies that have solved scaling problems:

You don’t need to memorise these. You need to understand the patterns and trade-offs.

2. Design before you code

Before starting any project larger than a weekend hack, spend 30 minutes sketching:

Even a rough diagram on paper helps you think through problems before they become bugs.

3. Ask “what happens when…”

These questions reveal design weaknesses before production reveals them for you.

4. Build progressively complex projects

5. Practice system design interviews

Even if you’re not interviewing, system design interview questions are excellent practice:

For each, practice thinking through requirements, components, data models, and scaling strategies.

Common mistakes

  1. Over-engineering from day one - Building for 1 million users when you have 100
  2. Under-engineering for growth - Making decisions that are impossible to change later
  3. Ignoring the data model - Your database schema is your architecture’s foundation
  4. Not considering failure - What happens when a service is unavailable?
  5. Copying without understanding - Using microservices because Netflix does, without having Netflix’s problems
  6. Premature optimisation - Caching everything before measuring what’s actually slow

The most important principle

Start simple. Measure real problems. Solve the problems you actually have, not the problems you imagine you might have.

The best architecture for a new project is almost always:

Then evolve deliberately as real constraints emerge. The developers who design good systems aren’t the ones who know the most complex patterns - they’re the ones who know when simplicity is enough.


Related reading: