Designing Frontend Systems (Part 1): High-Level Design for Spend-wise
Practical Introduction to Frontend System Design Through a Spending App

Introduction
When we talk about system design, most people think of large backend architectures, database scaling, or distributed systems.
However, frontend system design is just as important — especially as applications become larger and more complex.
Whether we're building a small tool or a large web app, having a structured design approach makes the project easier to maintain, scale, and use.
In this blog, I'm trying to decode the basics of frontend system design by working through an example project — Spend-wise, a simple spending tracker app.
Living in Bangalore, I realized I really needed a good app to track my daily expenses — but most options I found were either paid or packed with too many unnecessary features. So, as part of my learning journey, I decided to design one myself. Along the way, I'll walk through how I approached gathering requirements, scoping functional and non-functional needs, choosing a tech stack, and sketching a basic high-level design (HLD) to lay a solid foundation.
Understanding Requirements
The first step in designing any system is gathering requirements.
Before writing any code, it's important to know exactly what we are building and how it should work.
Skipping this step can lead to confusion, rework, and unexpected features.
There are two types of requirements:
Functional Requirements — what the app should do (features).
Non-Functional Requirements — how the app should perform (speed, security, responsiveness, etc.).
Both are important because they guide not just what we build, but how we build it.
Let’s simplify it.
Functional Requirements:
Add, Edit, Delete Expenses — Basic CRUD operations for managing expenses.
Validation — Ensure data is entered correctly (e.g., correct date format).
Categories — Allow users to categorize expenses (food, transport, etc.).
List View — Display all expenses in an organized, scrollable list.
Monthly Summary — Show a breakdown of expenses by month.
View Expenses in Date Range — Filter expenses by a custom date range.
Types of Expenses — Allow users to select expense types (fixed, variable).
Images of Bills — Attach photos of receipts or bills to each expense.
Daily Reminders — Notify users to add their expenses every day.
Help and Support — Provide assistance for users within the app.
Non-Functional Requirements:
Desktop/Mobile Support — Should work seamlessly on both desktop and mobile.
Offline Ready — The app should function without an internet connection (basic functionality).
Accessibility — Ensure the app is usable by people with disabilities (e.g., screen readers).
Internationalization — Support for multiple languages and currencies.
Authentication/Authorization — Secure login and user session management.
Backup — Regular backup of user data for safety.
Caching — Store frequently accessed data locally for faster performance.
A/B Testing — Implement features to experiment with different versions for user feedback.
Versioning — Track changes to app versions for updates and backward compatibility.
CI/CD — Continuous integration and delivery to streamline development and deployment.
Security — Protect user data with encryption and secure communication.
Testing — Thorough testing for both functionality and edge cases.
Performance — The app should load quickly, ideally within 3 seconds.
Scoping the System
Once we have the requirements, the next key step is scoping the system. Simply put, scoping is about deciding what to build first and what to save for later.
When starting, it's tempting to try building everything at once, but this often causes delays, frustration, and sometimes even quitting halfway.
That's why we focus on creating a Minimum Viable Product (MVP) first.
What is an MVP?
An MVP is the smallest, usable version of the app that includes the core features—just enough for users to find it valuable and for us to gather early feedback.
The aim is not to create a perfect app right away, but to build something functional, test it, and improve it gradually.
For Spend-wise, here's what I decided to include in the MVP:
Functional Scope (MVP)
Add, Edit, Delete Expenses — basic CRUD operations.
Category Selection — categorize expenses (food, transport, etc.).
Monthly Summary — show total spend per month.
Filters — filter expenses by category or date.
Listing Expenses — show expenses in an easy-to-browse list.
Non-Functional Scope (MVP)
Responsive Design — works well on both mobile and desktop.
Accessibility — basic support for screen readers and keyboard navigation.
Offline Support — app should work without internet (basic functionality).
Unit Testing — add tests for important functions and components.
Versioning — track changes and maintain app updates properly.
With this scoped MVP, we now have a clear starting point to move toward choosing the right tech stack.
Choosing the Tech Stack
Now that we know we need a lightweight, scalable stack, let's break it down piece by piece.
Selecting a tech stack early sets the tone for the entire project—how quickly we can build, how easy it is to maintain, and how scalable the app can become later.
Since Spend-wise is a relatively simple app (at least at the start), I'll keep the tech stack lightweight, modern, and easy to extend in the future.
Here's how I approached each decision:
Frontend Framework
React
I chose React because:
It's lightweight and component-based.
It has a large community and ecosystem.
I'm already familiar with it, which allows for quick prototyping.
Other options like Vue or Svelte could also work, but for this project, React seems like the best fit.
State Management
React Context + useReducer (for now)
Since Spend-wise won't have very complex data flows initially, heavy state libraries like Redux or Zustand are overkill.
I'll use:
Context API for global state (e.g., user settings, expenses).
useReducer for managing more structured state updates.
If the app grows more complex later (e.g., multiple pages, offline queues), I can always migrate to something more robust.
Folder Structure
src/
├── components/ # Reusable UI components
├── features/ # Core features like Expenses, Summary
├── context/ # App-wide context providers
├── hooks/ # Custom hooks
├── utils/ # Helper functions
├── assets/ # Images, icons
├── services/ # For persistence/API communication
├── styles/ # Global styles (Tailwind config, etc.)
├── App.jsx
└── main.jsx
Build Tools and Packages
Vite — Super fast bundler for React apps.
Tailwind CSS — For utility-first, responsive design.
React Router — To handle basic routing.
Vitest — For unit testing.
Testing Library (React Testing Library) — For testing components in a user-centric way.
ESLint + Prettier — Code quality and formatting.
Routing
React Router v6
Simple and declarative:
Home (
/) — View all expensesAdd Expense (
/add)Monthly Summary (
/summary)Settings (
/settings) (for future)
Even though it's a small app, structuring it properly early on saves a lot of time later.
CI/CD Setup
GitHub Actions
Run tests on every push.
Lint and format code.
Deploy automatically (Vercel).
Keeping CI/CD basic but effective in early stages.
Persistence / Local Storage
Since we're aiming for offline support and no server initially:
While LocalStorage is simple, it has a storage limit of around 5MB. For heavier data (like many receipts with images), we might later switch to IndexedDB, which can handle much larger amounts efficiently.
Later (post-MVP), can integrate with a backend or cloud storage.
Other Considerations
Basic Error Handling: Toast notifications or banners for success/failure.
Analytics (optional): For example, integrate basic event tracking if needed.
With these foundations in place, we’re ready to move into designing the low-level component design (LLD) for Spend-wise.
In the next post, I’ll walk through how I approach structuring components, handling offline-first strategies, and ensuring scalability from day one.



