My Core Philosophy: How I Design Digital Products that Scale
Scaling isn't about handling millions of users; it's about handling complexity. Here is my deep dive into modular monoliths, RLS, and shipping durable SaaS.
Most developers obsess over the wrong kind of scale. I see it all the time on Twitter and in the master's program classes I'm taking here in Ireland. Everyone wants to talk about Kubernetes clusters, microservices architectures that require a team of ten just to maintain, and handling "millions of concurrent users."
But here is the reality.
For 99% of us building SaaS products or indie hacking, you aren't Google. You aren't even Uber. If you design for ten million users on day one, you will fail before you get your first ten users. The complexity will kill you.
My philosophy on designing products that scale isn't about raw request throughput. It is about scaling me.
How do I, Emmanuel, scale my ability to ship features? How do I build a system that can handle a spike in traffic without waking me up at 3 AM? How do I transition from the "churn and burn" world of high-volume freelancing to building durable assets that compound over time?
This is a deep dive into how I approach product engineering now. It's a mix of rigid cloud engineering principles I'm learning academically and the scrappy, "ship-it-yesterday" mindset of an indie hacker.
1. The Database Schema is the Product
In my WordPress days, everything was a post or a meta field. It was flexible, sure. But it was a nightmare for data integrity. You couldn't enforce relationships strictly.
Now, when I start a new SaaS project (usually on Supabase + Next.js), I don't write a single line of frontend code until my database schema is locked in.
Scalability starts at the data layer. If your data structure is garbage, no amount of caching or edge functions will save you.
I rely heavily on PostgreSQL's strict typing and relationships. I don't let the application layer guess what the data looks like. The database is the source of truth.
Row Level Security (RLS) is Non-Negotiable
One of the biggest shifts moving to modern stacks like Supabase is realizing you can push security down to the database engine. This is huge for scale. It means I don't have to write verbose security checks in every single API route.
If I forget a check in my Next.js API handler, the database still blocks the request. That is peace of mind.
Here is how I typically structure a policy to ensure users only see their own organization's data. This keeps the app secure even if the frontend logic gets messy.
-- Enable RLS alter table "todos" enable row level security; -- Create a policy create policy "Individuals can view their own todos." on todos for select using ( auth.uid() = user_id ); -- More complex: Organization based access create policy "Org members can view org todos" on todos for select using ( auth.uid() in ( select user_id from organization_members where organization_id = todos.organization_id ) );
This pattern scales. It doesn't care if I have 10 users or 100,000. The logic lives close to the data.
2. Type Safety as Documentation
I used to hate TypeScript. It felt like homework. Why add types when I can just ship?
But when you are building a product that you want to grow for years, JavaScript is a liability. You forget what your objects look like. You forget if that user ID is a string or a number.
I use TypeScript not just for error checking, but as my primary documentation. I use tools to generate TypeScript types directly from my Supabase database schema. This creates a tight feedback loop.
If I change a column name in the database, my frontend build fails immediately.
This is how I keep velocity high. I don't have to manually check if my API response matches my frontend component. The compiler tells me.
Here is a script pattern I run constantly:
npx supabase gen types typescript --project-id "my-project-id" > types/supabase.ts
Then in my code, I never manually type interfaces for data. I extend them.
import { Database } from '@/types/supabase' type Todo = Database['public']['Tables']['todos']['Row'] const TodoItem = ({ todo }: { todo: Todo }) => { // I get autocomplete for todo.title, todo.is_complete, etc. return <div>{todo.title}</div> }
This seems small. But when you come back to a project after three months of studying for cloud exams, this type safety is the difference between shipping a feature in an hour or debugging undefined is not a function for two days.
3. The "Modular Monolith" over Microservices
I'm studying Cloud Computing. I know all about microservices. I know how to spin up twenty different containers in Azure and have them talk to each other over a message bus.
And I refuse to do it for my own products.
Microservices are for solving organizational problems, not technical ones. They exist so that Team A doesn't block Team B. I am a team of one. Maybe two.
I stick to the Modular Monolith architecture. I use Next.js. The frontend and the backend live in the same repo. They share types. They share utilities.
However, I keep them logically separated.
/apphandles the UI and routing./libhandles the business logic./componentshandles the display.
I treat my /lib folder like an internal SDK. The UI components shouldn't know how to fetch data directly from the DB. They should call a function in /lib that handles that.
This separation means that if I ever do need to break a piece off into a separate microservice (like a heavy video processing worker), I can just lift that logic out of /lib and move it. But I don't pay the microservice tax upfront.
4. Betting on "Serverless" (But understanding the limits)
Serverless is the indie hacker's cheat code.
Managing servers is a waste of my time. I don't want to patch Linux kernels. I don't want to configure Nginx. I want to write business logic.
AWS Lambda and Vercel Edge Functions allow me to scale to zero. If nobody visits my SaaS at 4 AM, I pay nothing. If I get featured on Product Hunt and 50,000 people show up, the infrastructure scales up automatically.
But there is a catch. Cold starts and connection limits.
Relational databases (like Postgres) hate serverless environments because serverless functions open a new connection for every request. You can exhaust your database connection pool in seconds.
This is why understanding the infrastructure is critical. You can't just blindly throw code at Vercel.
I use connection poolers (like Supabase Transaction Mode or PgBouncer). I ensure that my database isn't the bottleneck.
Also, I am careful about where my code runs.
If I'm building a dashboard for users in Europe, I stick my data in eu-west-1 (Ireland). I keep my compute there too. Latency kills user experience.
One trick I use to keep costs low while maintaining performance is heavy caching on the edge. Next.js makes this easy with the Fetch API.
// This request is cached at the edge for 1 hour. // Even if 10,000 people hit this page, my DB only gets hit once. async function getBlogPosts() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } }) return res.json() }
5. UI Architecture: Copy, Paste, and Own
For a long time, I used heavy component libraries like Material UI or Bootstrap. They were great until they weren't. You try to customize one dropdown and suddenly you are fighting specificity wars with CSS.
My philosophy now is: Own your UI code.
I use Tailwind CSS and Shadcn/ui. Shadcn isn't a library you install as a dependency. It's a set of components you copy and paste into your project.
This is vital for long-term maintenance.
If I need to change how a button works in my app, I don't have to wait for a library maintainer to update it. I just go into components/ui/button.tsx and change it.
It aligns with my need for freedom. I want total control over the pixel rendering without building everything from scratch.
Scale in UI isn't about how many components you have. It's about consistency.
I define a design system using Tailwind config. Colors, spacing, radii. Everything refers to variables.
// tailwind.config.js theme: { extend: { colors: { primary: 'var(--primary)', destructive: 'var(--destructive)', } } }
If I decide to rebrand later, I change the CSS variables, not 500 individual files.
6. Infrastructure as Code (IaC)
This is where my academic side bleeds into my indie hacking.
Clicking around in the AWS console or the Vercel dashboard is fine for a prototype. But it is not a strategy. You will forget which button you clicked. You will forget which environment variable you set.
I am moving more toward defining infrastructure in code. For AWS projects, I use Terraform or AWS CDK. For Vercel/Supabase, I use their CLI tools and config files.
Here is why this matters for design: Reproducibility.
I should be able to tear down my entire production environment and rebuild it in a different region with a single command. If I can't do that, I don't really own the system. The system owns me.
I keep my infrastructure definitions in the same repo as the code.
resource "aws_s3_bucket" "user_uploads" { bucket = "my-saas-uploads-prod" tags = { Environment = "Production" Project = "SaaS" } }
It feels like overkill for a small project. But when you accidentally delete a bucket (it happens), having this config saves your life.
7. The "Boring" Stack is the Best Stack
We love shiny new toys. I love playing with AI, vector databases, and the latest React hooks.
But for the core product? I choose boring technology.
- Postgres: It has been around for decades. It works.
- REST/GraphQL: Standardized.
- Cron Jobs: Simple time-based execution.
I avoid proprietary vendor-lock-in features unless they save me 10x the time.
For example, I use Supabase Auth because building authentication from scratch is a security risk and a time sink. That is a worthy trade-off. But I avoid using proprietary cloud databases that I can't export to a standard SQL dump.
Designing for scale means designing for portability. If Azure raises prices tomorrow, I need to know I can take my container and move to DigitalOcean or AWS.
8. Automating the Feedback Loop
Scaling products means scaling confidence.
I can't be confident if I'm manually testing every login flow before deployment.
I set up CI/CD pipelines using GitHub Actions immediately. It doesn't have to be fancy.
- Lint the code.
- Type check (TypeScript).
- Run a basic build.
If these three pass, I deploy.
I don't believe in 100% test coverage for startups. It's a waste of time. I test the critical path.
- Can a user sign up?
- Can a user pay me?
- Can a user perform the core function of the app?
If those work, ship it.
Here is a simple workflow I use for almost every project:
name: CI on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install dependencies run: npm ci - name: Type Check run: npm run type-check - name: Lint run: npm run lint
9. Observability: The Eyes and Ears
You cannot scale what you cannot measure.
In the freelance world, if a site went down, the client called me. In the SaaS world, I need to know before the users do.
I integrate logging early. Not just console.log. Structured logging.
I need to know why a request failed. Was it a timeout? Was it a permissions error?
I use tools like Sentry for error tracking or even just simple logging to a dedicated Slack channel for critical events (like a failed payment).
It allows me to sleep. I know that if something breaks, my phone will buzz. If it doesn't buzz, the system is working.
10. The Philosophy of "Enough"
Finally, the most important part of my design philosophy is knowing when to stop engineering and start selling.
Engineers, myself included, love to over-engineer. We want the code to be perfect. We want the architecture to support millions.
But a perfect system with zero users is a failure.
I design for the scale I have now, plus a margin for growth. I don't design for Google scale.
I build modularly so I can refactor later. But I don't optimize prematurely.
If a specific SQL query is slow, I'll optimize it when it becomes a problem, not before.
This is the balance.
The academic part of me wants perfection. The indie hacker part of me wants revenue. My design philosophy is the bridge between the two.
It is about building robust, typed, secure systems that are simple enough to manage alone but structured enough to handle success when it comes.
This is how I build now. No bloat. No guesswork. Just value.
Read Next
What I Learned About Business from 500+ Fiverr Clients
I completed over 500 orders on Fiverr. It was chaos. Here is what it taught me about business, scope creep, and why I pivoted to Cloud Engineering.
ReadThe 'Emmanuel Asika' Aesthetic: Why Minimalist Design Wins
Minimalism isn't just a visual choice; it's a survival strategy for Cloud Engineers and Indie Hackers. How stripping away complexity leads to faster shipping and scalable systems.
Read