My Engineering Principles: Own the Whole Problem
I build by owning the whole problem — design, code, ship, operate. Not just implementing specs. Here are the principles I've distilled from shipping six products solo.
After shipping six products — LetX, QuantumSketch, BikroyBuddy, Bagh, ComiKola, and offSchool — from a desk in Dhaka, certain principles have proven themselves repeatedly. These aren't management wisdom or conference talk platitudes. They're lessons from things that actually broke.
Own the whole problem
The most damaging thing a software engineer can do is treat themselves as a code-implementation unit. "That's a product decision" or "DevOps handles that" are failure modes, not professional boundaries.
When I'm building a feature, I own:
- The user problem being solved (not just the spec)
- The data model (not just the query)
- The deployment (not just the code)
- The on-call (not just the GitHub PR)
Owning the whole problem doesn't mean doing everything yourself — it means you never get to say "not my area." That accountability forces better design decisions. You write better APIs when you're the one who has to debug them at 2am.
Boring technology ships products
Every exotic technology choice is a bet that the novelty pays off in capability. Most bets lose. My tech choices:
| What I use | Why not exotic | |------------|---------------| | PostgreSQL | Every NoSQL DB I've tried added complexity, not capability | | Go | Rust is fascinating; I ship faster in Go | | Next.js | React meta-frameworks have converged; pick one and stick | | AWS ECS Fargate | Kubernetes is powerful; ECS is enough and simpler | | HTTP/JSON | gRPC is faster; the debugging simplicity of JSON > marginal perf |
The test: "Would I want to debug this at 3am after a production incident?" If no, don't use it.
Complexity is the enemy
Every abstraction has a maintenance cost. Every framework has breaking changes. Every dependency has security vulnerabilities. Before adding anything:
- Can I solve this with what I already have?
- If I add this, who maintains it in 2 years?
- What does this cost me when it breaks?
Three similar functions are better than one clever generic. You can read three functions. You can't always read the generic.
Tests that don't prove anything are worse than no tests
Tests give false confidence. A test suite that mocks everything and asserts green output is worse than no tests because it tells you the code ran, not that it works.
Tests I actually write:
- Integration tests that hit a real DB — every major feature has one
- Table-driven unit tests for pure functions (parsing, calculations, transformations)
- No mocks for infrastructure — if it's too painful to test without mocks, the architecture is wrong
I don't write tests for happy paths that I can verify by running the app. I write tests for edge cases that would be invisible in manual testing: empty inputs, concurrent writes, off-by-one in date math.
Ship incomplete, not wrong
An incomplete feature that works is better than a complete feature that has a subtle bug. When I'm under pressure to ship, I cut features, not quality.
Cut:
- Nice-to-have edge cases (handle the top 95% of inputs)
- Polish (good enough is good enough to learn from)
- Features no one asked for
Don't cut:
- Data integrity (write the DB constraint)
- Security (validate the input)
- Observability (log the error)
Fix root causes, not symptoms
When something breaks, the wrong response is to patch the symptom. The right response is to understand why it was possible for this to break.
LetX example: users were occasionally seeing stale document state after a reconnect. Symptom fix: add a "refresh" button. Root cause: the reconnect handler wasn't requesting the full CRDT op log, only ops since disconnect timestamp. Root cause fix: reconnect always requests full log from the server's current state vector.
The symptom fix would have shipped faster. The root cause fix eliminated an entire class of bugs.
Write for the person who debugs this at 3am
That person is you, probably six months from now. They have no context about what you were thinking when you wrote this code.
Comments I write: the WHY, not the WHAT.
// Use distributed lock here, not DB transaction —
// the compile job spans two separate DB connections (job table + artifact table)
// and they can't share a transaction across our connection pool.
lock := redis.NewMutex("compile:" + jobID)
I don't write comments that explain what the code does. Well-named variables and functions do that. I write comments that explain why an unexpected choice was made.
FAQ
What are your core engineering principles? Own the whole problem, choose boring technology, minimize complexity, write tests that prove things, ship incomplete not wrong, and always fix root causes. These have survived building six real products.
Why do you prefer boring technology? Exotic technology has higher learning costs, less community support, more edge cases, and breaking changes. The productivity benefit of familiarity compounds over time; novelty rarely does.
How do you balance shipping fast and writing quality code? By cutting scope, not quality. Remove features, not safety checks. A smaller, correct product beats a larger, buggy one.
What's your approach to testing? Integration tests over unit tests wherever possible, table-driven tests for pure functions, zero mocks for infrastructure. Tests must prove correctness, not just that code ran.
Written by Shihab Shahriar Antor — AI Engineer & Founder of Shahriar Labs. See also: The Solo Founder Stack · Build in Public: Lessons From 170+ GitHub Repos.