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.
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:
- Your app works for 10 users but crashes at 1,000
- Adding new features breaks existing ones
- Your database becomes a bottleneck you can’t fix without rewriting everything
- Debugging takes hours because no one understands how the pieces connect
- Scaling means “buy a bigger server” (which has limits)
With it:
- You make decisions about trade-offs intentionally, not accidentally
- Your app can grow without fundamental rewrites
- New team members can understand the system from a diagram
- You know where problems will appear before they do
- Scaling is planned, not panicked
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:
- Browser cache - static assets (images, CSS, JS) stored locally
- CDN cache - content cached at edge servers near users (Cloudflare)
- Application cache - results stored in memory (Redis)
- Database cache - query results cached by the database itself
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:
- Users: How many? Buyers and sellers or just buyers?
- Features: Listings, search, messaging, payments, reviews?
- Scale: 100 users? 10,000? 1,000,000?
- Constraints: Budget? Timeline? Team size?
Step 2: Identify core components
For a marketplace, you need:
- User system - registration, login, profiles
- Listing system - create, edit, search, browse products
- Messaging - buyers and sellers communicate
- Payment - secure transactions
- Search - find products by keywords, category, price
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?
users- id, name, email, password_hash, role (buyer/seller)listings- id, seller_id, title, description, price, category, statusorders- id, buyer_id, listing_id, status, paid_atmessages- id, sender_id, receiver_id, listing_id, content, sent_at
Step 5: Identify scaling points
Where will problems appear first as you grow?
- Search - Full-text search in PostgreSQL works up to ~100K listings. Beyond that, add Elasticsearch or similar.
- Images - Store in object storage (S3/Cloudflare R2), not your database.
- Payments - Use a third-party processor (Stripe, PayNow). Don’t handle money yourself.
- Real-time messaging - WebSockets for live chat. Can start with polling if simpler.
Step 6: Plan for what you don’t build now
Document decisions and their boundaries:
- “We’re using PostgreSQL full-text search. If we exceed 100K listings, switch to Elasticsearch.”
- “We’re server-rendering pages. If load exceeds X, add CDN caching.”
- “We’re using a monolith. If the team grows beyond 5, consider extracting the payment service.”
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:
- How does WhatsApp handle billions of messages?
- How does Uber match drivers to riders in real-time?
- How does YouTube serve video to billions of users?
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:
- What are the main components?
- How do they communicate?
- Where does data live?
- What happens when load increases?
Even a rough diagram on paper helps you think through problems before they become bugs.
3. Ask “what happens when…”
- What happens when this server goes down?
- What happens when many more users arrive?
- What happens when the database is full?
- What happens when two users try to buy the last item simultaneously?
These questions reveal design weaknesses before production reveals them for you.
4. Build progressively complex projects
- Simple: Blog with authentication → teaches client-server, database, sessions
- Medium: Real-time chat app → teaches WebSockets, scaling connections
- Complex: Multi-vendor marketplace → teaches payments, search, multiple user types
- Advanced: Distributed task queue → teaches event-driven architecture, reliability
5. Practice system design interviews
Even if you’re not interviewing, system design interview questions are excellent practice:
- Design a URL shortener
- Design a chat application
- Design a notification system
- Design a file storage service
For each, practice thinking through requirements, components, data models, and scaling strategies.
Common mistakes
- Over-engineering from day one - Building for 1 million users when you have 100
- Under-engineering for growth - Making decisions that are impossible to change later
- Ignoring the data model - Your database schema is your architecture’s foundation
- Not considering failure - What happens when a service is unavailable?
- Copying without understanding - Using microservices because Netflix does, without having Netflix’s problems
- 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:
- One backend service
- One relational database
- One frontend
- Deployed to one platform
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:
- How to Build Your First AI Agent - apply system design thinking to building an AI agent from scratch
- How to Become a software engineer in Zimbabwe - the full roadmap from beginner to working developer
- Best Programming Languages to Learn in Zimbabwe - choosing the right language for your career goals