Skip to main content
Home Creations Blog About Contact
Indie Engineering 8 min read

The Solo Engineer's Playbook: Shipping Production Software

Working alone changes which engineering practices pay for themselves. The constraint is not skill or time — it is that you are the only person who will ever be paged, and everything follows from that.

R
R3X Software Engineer & Creator

Most engineering advice assumes a team. Code review catches your mistakes, someone else is on call this week, and the architecture astronaut is balanced by the pragmatist across the desk.

Alone, none of that holds. The practices that pay for themselves are different, and some widely-recommended ones are actively wrong for one person. What follows is the set that survives contact with actually running something by yourself.

The governing constraint

You are the only person who will ever be paged.

Every architectural decision should be evaluated against that. Not "is this elegant", not "does this scale", but: when this breaks at 11pm on a Saturday, how quickly will I understand what happened and how quickly can I fix it?

This reframing kills a surprising number of otherwise-attractive ideas. Microservices are not wrong; they are wrong for one person, because the failure modes are distributed and debugging them requires correlating across services at exactly the moment you are least equipped to do it.

Choose boring technology, deliberately

The argument is not that new technology is bad. It is that novelty has a cost paid at the worst possible time.

When something breaks in Postgres, Laravel, or nginx, the answer is on the internet already. Someone has hit it, written it up, and been corrected by three people in the comments. When something breaks in a tool with four hundred GitHub stars, you are reading the source at midnight.

A useful budget: pick at most one interesting thing. If the product idea is novel, the stack should be dull. If you genuinely want to use the new runtime, build something whose domain you understand completely.

Concretely, for a solo product:

  • One application framework you know well.
  • One database. Postgres, or SQLite if the shape fits.
  • One queue, one cache — ideally the same Redis.
  • One server, or one PaaS.

That is a stack you can hold entirely in your head, which is the actual requirement.

Automate in order of pain, not in order of virtue

The instinct is to build a full CI/CD pipeline before the first user. That is optimising for a problem you do not have.

Automate in the order things hurt:

First: deploys. You will do this hundreds of times. A deploy that takes ten manual steps will eventually be done wrong, at speed, during an incident. Even a shell script is transformative — the point is that it is repeatable, not that it is sophisticated.

Second: backups, with restore tested. Not the backup — the restore. A backup you have never restored is a hypothesis. Restore into a scratch database quarterly and confirm the row counts.

Third: the tests that cover money and data loss. Not full coverage. The paths where a bug is expensive: payment handling, permission checks, anything that deletes.

Fourth: alerting on user-visible symptoms. Error rate and latency on your critical path. Not CPU, not memory — those are things you look at after an alert, not things that should wake you.

Everything else is optional for a long time. Full coverage, elaborate pipelines, staging environments that mirror production — these are solutions to coordination problems, and coordination is not your problem.

Tests: where they pay, and where they do not

The rules for a team assume tests are also communication. Alone, they are only a safety net, which changes the calculus.

High value:

  • Integration tests over critical flows. One test that exercises signup end-to-end catches more than twenty unit tests over its parts.
  • Regression tests for every production bug. This is the highest-return testing you can do. You already know it can happen, and the test costs ten minutes while you have the context.
  • Tests over anything involving money, permissions, or deletion.

Low value:

  • Unit tests over trivial code. A test asserting a getter returns the value is maintenance with no upside.
  • Mocking your entire dependency graph. Brittle, and it tests your mocks.
  • Chasing a coverage percentage. Coverage measures execution, not verification.

The honest position is that a solo project with 40% coverage concentrated on the paths that matter is in better shape than one with 85% spread evenly.

Design for debuggability over elegance

You will spend more time understanding what happened than writing the code. Optimise for the former.

Log decisions, not steps. Why the system chose what it did, with the inputs to that decision. This is covered in more depth in structured logging that works, and the short version is that a log line naming the rule, the threshold, and the value settles a support question in seconds.

Prefer explicit over clever. Metaprogramming, deep inheritance, and magic that saves keystrokes all cost you comprehension later, when you have forgotten everything. Code you can read at 2am beats code you were proud of.

Keep the request path shallow. Every layer between the HTTP request and the database is a layer to step through. An abstraction earns its place by removing genuine duplication, not by anticipating change that may never come.

Make state visible. An admin page listing recent jobs, failed jobs, recent errors, and current queue depth is an hour of work and it is the first thing you will open during every incident.

What to monitor when nobody else is watching

Four alerts. Resist adding more, because alert fatigue arrives faster when there is one person receiving them.

  1. Error rate above threshold, sustained for five minutes. The for clause matters — a single bad minute is not an incident.
  2. p99 latency on the critical path. Users notice slowness before they notice errors.
  3. Oldest-job age in the queue. Background work failing silently is the classic solo blind spot, because nothing visibly breaks.
  4. A synthetic check of the core flow, from outside your infrastructure. Everything green while the site is unreachable is the outage you find out about from a customer.

That fourth one catches an entire class of failure that internal monitoring cannot see — DNS, certificate expiry, a CDN misconfiguration, a firewall rule.

Add a dead man's switch for anything scheduled. A cron that stops running produces no alert, because nothing failed:

Schedule::call(fn () => Http::get(config('services.deadmanssnitch.url')))
    ->hourly();

If the ping stops arriving, the monitoring service alerts you. Silence becomes a signal.

Where to deliberately cut corners

Being explicit about this is more honest than pretending you do everything properly.

Acceptable to skip, at least initially:

  • A staging environment that mirrors production. Feature flags and careful deploys get you most of the way.
  • Blue/green deploys. A brief maintenance window at 4am is survivable for most products.
  • Horizontal scaling. Vertical scaling goes considerably further than people expect, and it is one machine to reason about.
  • Multi-region. Enormous complexity for availability most solo products do not need.
  • Full observability tooling. Structured logs plus four alerts covers most incidents.

Not acceptable to skip:

  • Backups with tested restores.
  • HTTPS everywhere and dependency updates for security patches.
  • Idempotency on anything charging money.
  • A rollback path that does not require thinking.

The distinction is whether the corner you cut costs you time or costs you data. Time is recoverable.

Managing the deploy you make alone

Without a reviewer, the risk of a bad deploy is entirely yours to manage. Three habits cover most of it.

Deploy small and often. A deploy containing one change has one possible cause when it breaks. A deploy containing three weeks of work is a bisection problem during an incident.

Never deploy before you go away. Friday afternoon is a cliché for a reason. If nobody will be watching for the next twelve hours, it can wait until Monday.

Have a rollback that takes seconds. Symlink-based releases with the previous version still on disk turn a bad deploy into a thirty-second event rather than a rebuild. The details are in zero-downtime Laravel deploys; the important part is that rollback must not require thinking, because you will be doing it while stressed.

And the corollary that catches people out: rolling back code does not roll back the database. Write migrations that work with the previous release, or your rollback path is a restore from backup — a completely different class of event.

The sustainability question

The failure mode for solo products is rarely technical. It is the maintainer running out of energy.

A few things that measurably help:

Cap the on-call surface. If you cannot sustain being paged at 3am indefinitely — and you cannot — then either the product tolerates a few hours of downtime, or it needs more than one person. Decide which, explicitly, and set customer expectations to match. An SLA you did not promise is one you cannot fail.

Write things down as you go. Not documentation for others — notes for yourself in three months. Why this queue is separate. Why that timeout is 45 seconds. Why this seemingly redundant index exists. You will not remember, and rediscovering it costs an afternoon each time.

Delete features. Every feature is permanent maintenance. A feature used by four people costs you attention forever. Removing it is engineering work, not failure.

Automate the recurring annoyance, once. Anything you do manually more than monthly. Not because it takes long, but because the friction accumulates into reluctance, and reluctance is what stops projects.

The playbook, condensed

  • Evaluate every decision by how debuggable it is at 2am, not how elegant it is at 2pm.
  • One interesting thing, maximum. Everything else boring on purpose.
  • Automate deploys first, restores second, expensive-path tests third, symptom alerts fourth.
  • Test the paths where bugs cost money or data. Ignore coverage as a target.
  • Four alerts, one of them synthetic and external, plus a dead man's switch on cron.
  • Cut corners on scale and process. Never on backups, rollback, or security patches.
  • Deploy small, deploy often, never before you disappear.
  • Write down why, not what.

None of this is advice for a team, and some of it would be wrong for one. That is the point — the constraint that shapes solo engineering is not the amount of work, it is that there is nobody else to catch it.

Related articles

6 min read
Laravel & Architecture

Why SQLite is Often the Best Database for Modern Creator Platforms

The case for SQLite is not that it scales further than you think. It is that the operational cost of the alternative is far higher than most small products can justify — and that cost is paid every month, forever.

#Database #Laravel #SQLite
Read
10 min read
Systems Engineering

Zero-Downtime Laravel Deploys: The Parts That Actually Bite

Atomic symlink swaps are the easy half. The failures that take a site down mid-deploy come from opcache, queue workers running old code, and migrations that lock a table.

#Laravel #DevOps #Architecture
Read
9 min read
Databases

Running SQLite in Production: What Actually Breaks

SQLite handles far more production traffic than its reputation suggests — but the failure modes are specific, and most of them trace back to two settings people never change.

#SQLite #Database #Architecture
Read