These are first-person field notes about building and operating software. They cover small systems, source reading, performance, local machine learning, dependencies, reliability, and the habits that shape the work. The excerpts below are present in the raw HTML so readers and crawlers do not need JavaScript to discover the substance of the site.
Before I write any implementation, I write the function signature. The API. The interface. The boundary between what it does and how it does it.
If the interface is clean, the body usually follows. I can see the types, the names, the error path, and the insides have a place to land. If the interface is awkward, no amount of clever code inside will fix it. Callers will still pass seven arguments in the right mood. They will still call step two before step one. They will still read the comment I wrote because the signature would not speak.
I have thrown away entire implementations and kept the interface unchanged. The callers never knew. That is the point. The interface is the contract. Everything else is negotiable.
In 2024 I started porting ML models to Apple Silicon using MLX-Swift. I expected it to be a compromise. Run models locally, sure, but slowly.
It wasn't slow.
Unified memory is the whole trick. The GPU and the CPU share the same RAM. On the old cloud box every tensor had a trip. Write it on the host. Copy it over the bus. Compute. Copy it back. The copies were the day.
PostgreSQL. Redis. A Linux server. That is the stack for most of what I will ever ship. I keep saying it like a joke. I mean it as a budget.
I spent years chasing new databases. Tried CockroachDB, FaunaDB, SurrealDB. Each one had a compelling pitch. Multi-region this, no-ops that, a query language that would finally feel like the app. Each one had weird edge cases nobody documented because nobody had hit them yet. I hit them, with no page to steal.
PostgreSQL has been hit with everything. Every edge case is written down. Every weird behavior has a thread from 2014. I used to hear that as dull. I hear it now as a list of ways the thing has already failed, in public, with a workaround. I can list the main ways it will let me down before I put data in it. That is the whole prize.
In 2021 I stopped coding for three months. Didn't touch a keyboard. Didn't think about software. Just existed.
When I came back everything was clearer. Problems that felt impossible before were obvious. Code I'd been stuck on for weeks wrote itself in an afternoon.
I had been sitting inside those problems every day. The same files. The same wrong shape. A weekend off does not move that. The loop is still loaded when you open the laptop on Monday. Three months emptied it. I could see the extra layer I had been defending, the function that should have been deleted, the approach that only made sense because I was tired of starting over.
The fastest computation is the one you don't do.
My semver library is fast because it precomputes. My bots are fast because they cache user data. This website is fast because there is nothing to compute at all. Three shapes. Same move. Do the work once, keep the answer, hand it back.
I have replaced n log n sorts with hash lookups more times than I can count. Not because I am clever. Because the data did not change often enough to justify lining it up again.
When something breaks, I don't reach for the debugger. I read the code.
Not skim. Read. Start from the entry point. Follow the data. Ask: what did I think this does? What does it actually do? The bug lives in the gap between those two questions.
A failure on a huge input is a bad place to think. Shrink it until the same wrong answer shows up on something you can hold. Half the file. One request. One row. If the small case is clean, the bug is in whatever the small case dropped, and you just learned that without opening a tool.
The best commit I made last month was 400 lines of deletions.
There was a feature nobody used. I checked the analytics. Zero hits in 90 days. I deleted it. The codebase got simpler. The tests got faster. One less thing to maintain.
I almost left it. The code still compiled. The tests still passed. Removing it meant touching a route, a template, a flag, a couple of helpers that only existed for that path. The honest reason I hesitated was loss. I wrote that feature. Deleting it felt like admitting the bet was wrong. The analytics did not care. Zero is zero.
I used to npm install everything. Need to pad a string? Package. Parse a date? Package. Check if a number is even? Believe it or not, package.
Then I would open node_modules and find 800 folders for a project that sends emails.
The turning point was a security audit. I had to explain every dependency. Why do we need this? What does it do? Can we trust the maintainer? I could not answer for half of them. I had installed packages I had never read a single line of.
The worst bugs are the ones that do not crash.
A null pointer exception at 3am is annoying but simple. I get a stack trace. I find the line. I fix it. A function that silently returns wrong data can run for weeks before anyone notices. By then the damage is in the database, in the cache, in the emails we already sent. The original lie is gone. I am debugging a neighborhood.
I write code that crashes early. If something is wrong, I want to know in the same breath. Not in a log file nobody reads. Not as a subtle inconsistency that looks like a real value. A loud, obvious failure, close to the line that broke the rule.
My average commit is 15 lines. Some are 3. Rarely more than 50.
Each commit does one thing. Rename a variable. Fix a bug. Add a function. Not "refactor auth module." That's five commits pretending to be one.
I used to save up a whole afternoon and dump it in one hash. The message was a shrug. The diff was a weather system. I could not revert the rename without also reverting the bugfix. I could not ship the bugfix without dragging the half-finished rename onto main. So I sat on all of it until Friday, then asked someone to "take a look."
The Indian tech scene has a reputation problem. Cheap outsourcing. Body shops. "Do the needful."
I ignore all of it. I charge $1,000/day. I turn down work that isn't interesting. I open source everything I can.
The rate is a filter before it is a number. People who want a warm body at a discount bounce off it. People who want a specific person stay. I would rather have fewer days and keep the work I actually want to think about. The uninteresting work pays the same hour and spends a different kind of money. Attention. Taste. The next six months of what I am willing to open.
I never finished a programming course. I've started dozens. Never finished one.
What I did instead: I built things. A bot that tells you the weather. A search engine for my bookmarks. A CLI tool that does one thing I needed.
Each one taught me exactly what I needed to know, exactly when I needed to know it. Not "here's how arrays work" in week 3 of a curriculum. But "I need to sort these results by relevance, how do I do that?" at 1am on a Tuesday because I'm obsessed with my side project.
You should not be prompting coding agents anymore. You should be designing loops that prompt them.
I was late to that. For a long time I treated the agent like a very fast junior. I asked for a plan. I read it. I said do part one. Then part two. I spun up another agent for review and copied the feedback back by hand. I was the loop. I carried the context, decided the order, made sure nothing fell through. It worked. It did not scale with me.
The first version I tried was almost a joke. A goal file, a progress file, and a while loop that started a fresh agent every time the last one exited. Same prompt. Clean context. The only memory was on disk: the code, the git log, a short note about what already failed. That is the whole trick. The model forgets. The repo does not.
I keep coming back to this: the best code is the code I didn't write.
If this line doesn't need to exist, it shouldn't.
Every line I add is a line I have to maintain. A line that can break. A line someone else has to read and understand. Including future me, who will have forgotten why it's there. The line is doing work today. It is also a bill that arrives every time I touch the file, run the tests, or explain the system.
I spend more time naming things than writing the code inside them.
A function called processData tells me nothing. A function called parseVersionString tells me everything. The second one does not need a comment. The first one needs a paragraph, and even then I will open the body the next time I see the call. The name is what the call site has. If the name is empty, every reader pays the full cost of the function again.
A good name is compression. It packs the job into a few characters so I can keep the body closed. I already do this with loops and maps. I do not reread forEach to remember it walks a list. I want my own functions to work the same way. A chunk I can hold. A debt I do not reopen.
Most meetings should be a document.
A meeting takes an hour from five people. That's five hours gone. A document takes 30 minutes to write and 10 minutes each to read. Same information, four hours saved. The calendar still looks full if you only count rooms. The week is empty of the work.
I've worked with teams that had no meetings at all. Just shared documents, pull requests, and short messages. We shipped faster than any team I'd been on before. Nobody waited on Tuesday at 3. The page was already there when you sat down.
This website is one HTML file. The CSS, the JavaScript, the data, the components, the router. All in one file.
People ask why. The answer: why not?
One file means no build step. No bundler config. No module resolution. No "it works on my machine." Open the file, it works. Deploy the file, it's live. The path from edit to browser is a save and a refresh. I can break the page and see the break in the same breath. I can host it anywhere that will serve a file.
One box. A cheap 4-core ARM machine. It runs everything I have ever shipped and it is boring. Four months of uptime, a third of the disk used, memory basically empty. The box is barely awake. Until the disk, the CPU, or the memory is actually full, a second machine is just a second thing to understand.
People will tell you you need Kubernetes. A CI pipeline. A container registry. A managed Postgres that bills you by the hour. For a handful of side projects. This is insane. You need a reverse proxy and a little discipline. That is the whole stack.
One process is on the internet. Caddy. It does TLS for you, gets the certs, renews them, forwards each domain to something on localhost. No certbot. No renewal cron. No 3am page because a cert expired. You write down a domain, you get HTTPS. Done.
My most popular repo has 500 stars. It took four years to get there.
Year one: 12 stars. All from friends. Year two: 40. A few strangers found it. Year three: 150. Someone mentioned it in a blog post. Year four: 500. It showed up on a front page for 20 minutes.
From the outside it looks like it blew up overnight. From the inside it was four years of quiet commits, fixing issues nobody reported, and improving docs nobody read yet. The spike was a door opening onto a room I had already furnished. If the install was broken that week, the twenty minutes would have been a screenshot of a 404.
I used to think building things was how you got good. Building is half of it. The other half is sitting in a working system and watching how it actually moves.
Not a post about the code. The code.
I learned more from reading the Redis source than from any database course. About 50,000 lines of clear C. You can hold the shape of it. Every function does what its name says. The comments explain why.
I'm in India. Most of my clients are in the US or Europe. People treat the gap as a problem I should apologize for. The gap is the product.
When my American clients go to sleep, I start working. When they wake up, the thing they asked for is done. It feels like magic to them. It's just timezones. Their evening ticket is my morning. Their morning inbox already has a diff.
The same clock that looks awkward on a calendar is a free overnight shift if the work can move without a voice on the line. I get a quiet block. They get a finished thing. Nobody stays up to perform overlap.
I rewrite things a lot. Not because the first version is bad. Because I understand the problem now.
The first time you build something, you're learning the domain. You make wrong abstractions. You over-engineer some parts and under-engineer others. You can't know the right design until you've built the wrong one.
The rewrite starts informed. I already paid for the wrong map.
I picked up Rust in 2023 expecting a language. I got a way of thinking.
The borrow checker keeps asking one question: who owns this data? I had never asked that before. In Python and JavaScript the answer was easy. Who cares. The garbage collector will figure it out. In Rust I have to know. Each value has one owner. There is only one at a time. When the owner leaves scope, the value is dropped. I can lend it, mutably to one place or shared to many, and I cannot do both at once.
At first the compiler felt like a hall monitor. I would write the shape I was used to, a graph of objects that all point at each other and mutate when they feel like it, and the compiler would refuse. I cloned things to make it shut up. I wrapped things so two owners could pretend to share. The code compiled and I had learned nothing.
Every npm install parses version strings. Thousands of them. Installing preact alone calls semver 21,000 times. One package.
The semver package has 150 million weekly downloads. And it is doing way more work than it needs to. A parse walks a regex, allocates an object, splits prerelease tags, and throws if the string is ugly. That is the right work for a string you have never seen. It is the wrong work for 1.0.0 the twenty thousandth time in the same install.
Version numbers aren't random. React is at 18. Node is at 22. Most packages never pass major version 50. The input space is small and almost still. So I precomputed all of them.
2017: Telegram bots in Python. Everything in one file. Deployed by SSH-ing into a $5 server and running python bot.py.
2018: Discovered Node.js. Rewrote everything. Thought callbacks were fine. They weren't.
2019: Built an image search engine. Learned about vectors, embeddings, and why databases are hard.
The fastest teams I've worked with shipped the smallest changes. A 30-line diff is easy to review, easy to test, easy to revert if it breaks. A 3000-line diff is a prayer.
I used to batch things up. I'll ship all of this together when it's ready. It was never ready. The pile grew a refactor, a half-done feature, a bug I noticed in passing. When I finally pushed, something always broke, and I couldn't tell which part did it. The revert was a coin flip. I sat in the logs guessing.
Now I ship constantly. Multiple times a day sometimes. Each change does one thing. If it breaks, I know exactly what and exactly why.
Everything good in my career came from a side project.
My first job came from a Telegram bot I built for fun. My freelance clients found me through open source repos. My best technical skills came from projects nobody asked me to build.
The bot was running. Someone could talk to it. That was enough to start a conversation that became a job. Later a stranger would clone a repo and write me. I did not send a deck. I sent a link. The link had to do the talking while I was asleep.
Every abstraction is a loan. I borrow a little simplicity now and pay complexity later. The payment is the extra name, the extra file, the extra hop through a function that only exists to hide the hop. I pay it every time I come back to the code, and every time someone else has to learn the map before they can change a line.
Sometimes the loan is worth it. A well-placed layer saves hours of copy-paste and keeps one rule in one place. Most of the ones I have added were early. I could already see four implementations. I built the factory first. I ended up with one. Now everyone has to understand the factory to reach the one thing that actually runs.
I count abstractions the way I count dependencies. Each one has to justify staying. A dependency can sit unused in a lockfile and I will still feel it on upgrade day. An extra type, an extra interface, an extra manager class does the same thing to my head. If I cannot explain why this layer exists in one sentence, I flatten it.
Every millisecond you waste is a millisecond of someone's life.
That sounds dramatic. Multiply it. A page that takes 2 seconds instead of 200ms, loaded 10,000 times a day. That is 5 hours of human life. Every day. On one page. You will never meet those people. They still paid.
A click that comes back before you lift your finger feels like your hand did the thing. Past a second your mind leaves the sentence you were reading. You check another tab. You forget why you opened this one. The work is still in flight. The person is not. Ten seconds and they have left the page for good.
I started building Telegram bots in 2017 because I was bored. Wrote one that replied to messages with random quotes. Took maybe an hour.
That was the first one that stayed running.
Seven years later, some of my bots handle millions of messages a day. I have learned more about building reliable systems from those bots than from any job or course. The hard parts never lived in the reply handler. They lived in everything that had to keep working after the process died.
If a dependency is less than 100 lines of code, I write it myself.
I want 100 lines I understand completely more than 100 lines I don't. A package is those lines plus a name on a registry, a version range, a lockfile entry, a person who can unpublish, a publish token that can leak, and every package that package pulls in. I take the whole chain when I type the install command. I rarely want the chain.
Someone unpublished an 11-line pad helper. It prepended characters to a string in a loop. Builds failed across millions of projects. I could have written those 11 lines in 30 seconds. So could you. So could anyone. But 2.5 million projects depended on one person not deleting their npm package. The helper was never the hard part. The hard part was letting a registry own a loop.
I write different code in Rust than in Python. Not just syntactically. Structurally. The language shapes how I cut the problem before I have typed a useful line.
In Python I reach for a dict and a script. I prototype fast and sloppy. I will know the shape when I see it run. In Rust I design upfront because the compiler will not let me be sloppy. I have to name the owner, the error, the lifetime, before the first happy path compiles. In TypeScript I lean on types to write the intent down and let the checker nag me when a field goes missing. Each language is a different lens on the same job.
A language does more than make some ideas easy. It makes some ideas hard to avoid. Rust will not let me skip ownership. TypeScript will not let me skip the shape of the value, at least not without writing any and feeling it. Python will not force either, so I can spend a whole afternoon without deciding. That freedom is why I open it when I am still lost. It is also why the lostness can last.
I write the types before I write the logic.
A type signature is a compressed specification. This one does most of the talking:
fn parse(input: &str) -> Result<Version, ParseError> It takes a string. It might fail. When it succeeds, I get a Version, not another string I have to treat carefully. When it fails, I get a reason. No comment does that in fewer characters. Unlike a comment, the compiler checks it. It cannot drift. It cannot lie.
The first version of everything I've shipped was embarrassing.
My first Telegram bot was 200 lines of spaghetti Python with hardcoded strings. It worked. People used it. I cleaned it up later.
My first Rust CLI was a single main.rs with no error handling. It worked. I used it daily. I refactored it later.