Building low complexity SaaS
Stay sane, stay afloat, stop trying to be cool
Something that I bang my head on the table with most days, is the potential for spiralling costs when building anything web related.
With serverless and lightweight infrastructure, the potential to bankrupt yourself is unbelievably high, along with ‘complexing yourself insane’. In an effort to keep things as simple as possible, I’ve started developing patterns around the smallest number of components possible, because I don’t care about web-scale and if an API server goes offline, I’ll get an alert and mend it pretty darned quick.
If you’re asking yourself about needing Kubernetes, I’ll end the pain; you don’t need it. The requirements leading to Kubernetes become self-fulling. If you need it, you know you need it. Containers or small VMs (like Firecracker) become however useful deployment building blocks and can make your life easier.
My stack typically looks like this and where I can, I prefer other people’s computers:
React or NextJSA UI app is on Vercel or AWS Amplify (their problem, not mine)
Authentication is either Auth0 or Clerk and has predictable costs
API binary is ran an EC2 instance, but I’m thinking more about Fly.io because of their IP Anycast capabilities
The binary is monitored by Grafana Cloud and is wired for Prometheus
Database is DynamoDB because it’s simple and I don’t have to water or feed it
DynamoDB is fast and flexible with many use case capabilities
Object Storage is AWS S3 or Cloudflare R2
Both are fast, R2 is cheaper and S3 API compatible
DNS I try and keep native to wherever I bought the domain. Route53 costs money!
Third party services I like to be credit driven. I cants spends what coins I didn’t puts in. In other words, upfront payment for things like SMS gateways and transactional email systems. Each user subscription can have a set of tokens which can be spent every billing period to provide access to these services through your API code.
The invariables (UI, Auth, API binary) allow me to sleep at night and so the only variable becomes DynamoDB.
Also, component re-use is high, which means my pet projects can be rapidly mended, improved, upgraded and easy to understand because everything kind of looks the same.
Constraining Costs
Death by a million reads or writes and leaky buckets
Read where possible to limit write attempts
Instead of blindly throwing data at DynamoDB with conditionals, I’ll read records first with a constrained projection, to make sure the record has space for the new data and will accept it. If not, I create a new record. Over time, this saves money despite increasing latency due to the number of database calls. If the calls are made from an EC2 instance or Lambda function, the round trip API time is negligible in comparison to that of UI to API.
API rate limit by IP
If you serve multiple users, then for the love of all things holy, rate limit your API by IP address. That way when users are wildly hitting refresh, damage against the database and object storage system (i.e., cost) is limited.
Age out records
Unless you’re charging users for leaving their data lying around, then age records out aggressively. Typically I expire user records at thirteen month periods (and any image or file data too). That includes invoice data and analytics. Thirteen months is enough for doing year by year comparisons and if you enable TTL
on a table and set the field, you have little to no clean up to do, saving you more money because a TTL deletion is free and records magically vanish!
Limit the file object functionality and access
Do users need to store that file or image? How long do they need it for? Can it be charged for with enough buffer to prevent budget problems?
Limit the file upload size
Limit the number of downloads in a billing period
Meter the usage and tokenise access
Each user has a monthly budget, calculated from their subscription fee for data
Transfer and storage costs for a number of uploads and downloads is calculated ahead of time
These budgets are then managed by your code and if the user runs out, send them an upgrade email or top-up request
Don’t go just yet…
You’re probably not internet scale
Keep any financial commits as low as possible, because, unless you work for a FAANG, the changes are your projects will have very low requirements and make the most of it. It’s no bad thing. I run most of projects on a single EC2 instance because it if breaks (which they rarely do) I can deploy another one rapidly.
I do run a load balancer, because if the machine goes down or if I need more horsepower (upgraded EC2 or more of them), then I can just add another machine behind it without breaking DNS.
CICD
This isn’t me trying to be cool. Git commits trigger deployments. Simple. It means I can also scale-up or down on demand. I don’t worry about scale groups or automatically scaling, because my pet projects have few users. That’s why they’re called pet projects!
We all love a low cost entry point to using software and depriving existing or potential users of functionality & risking your own pocket shouldn’t be a thing.
Summary
It’s easy to get carried away building things and by using as few components as possible, I can make the daily pet project hour I have available, go a very long way. My projects are pocket money activities and I can spend pocket money on them, or make pocket money back. Professionally, I see many efforts at trying to be cool, or building for a future that won’t happen. Building it doesn’t mean they will come. Half the time, your potential users don’t even know you exist, never mind where you are. Fancy & aspirational thinking should be reserved for marketing!