Building SaaS Taught Me That Code Is the Easy Part
I started by thinking the hard part of SaaS would be engineering. Then users, billing, support, infrastructure, and edge cases showed up.
When I started building SaaS products, I thought most of my time would go into writing code.
That was wrong.
Code is usually the part I understand best. The hard part is everything around it.
A feature is never just the happy path
Take subscriptions.
At first it sounds like this: the user clicks a plan, payment succeeds, the plan is active.
Then the other cases show up.
What if payment succeeds but the webhook is delayed?
What if the user already has an account?
What if they paid before creating an account?
What if the subscription is cancelled?
What if a payment fails?
What if the user upgrades halfway through a billing cycle?
What if the database is unavailable when the webhook arrives?
At that point, add billing is not one feature. It is a small distributed system.
The same pattern shows up elsewhere
Scheduling a post sounds like a calendar UI.
It is also:
- timezone handling
- duplicate prevention
- retries
- a queue
- failed jobs
- provider limits
- idempotency
- status tracking
- telling the user what happened
AI content generation sounds like one API call.
It becomes:
- model selection
- prompt versions
- token cost
- rate limits
- validation
- fallbacks
- evaluation
- logs I can actually read
This is why a SaaS product gets complicated quickly.
I design the failure path first
This is the biggest change in how I build.
Before I ask what happens when everything works, I ask what happens when this fails.
If the payment provider is down, what does the user see?
If the model times out, can the job be retried safely?
If a scheduled task runs twice, do we create two posts?
If a webhook arrives twice, do we apply the event twice?
These questions are not interesting. They are where production systems usually break.
A small product is still a system
When a product has 50 users, it is tempting to ignore architecture.
Sometimes that is correct. I should not build infrastructure for traffic I do not have.
I still want clear boundaries.
I want to know where billing ends, where the application begins, where AI execution happens, and where background jobs live.
The implementation can stay simple. The boundaries should still make sense.
What I count as useful engineering
I still like writing code. Building products changed what I treat as good engineering.
Good code that solves the wrong problem is not useful.
A clever architecture that nobody needs is not useful.
A polished feature that creates support problems is not useful.
I am not trying to build the most impressive system. I am trying to build the smallest system that solves the real problem and can handle the next one.
That is less exciting. It is closer to how the work actually goes.