Category Archives: Software Development

Dev Stack 2025: Part III, Django

I learned Django in 2006 and used it as main way to make web applications for side projects until 2021, when I decided to move to node/express for my backend. It’s time to go back.

As I mentioned yesterday, my stack changes are driven by the prevalence of supply chain attacks and my interest in Agentic AI software development. NPM/Node seems especially vulnerable to these attacks, which is why I am leaving that ecosystem. I considered Rails and Django. In the end, even though I think Rails may be doing more things right, I already know Python, use it for other projects, and Django is close enough.

To me, the main reason to pick Rails or Django in 2025 is that it provides good defaults that can be constraints for using AI. When I see vibe coded projects, the AI they use prefers node/express, which lets them do anything, including things that they shouldn’t do. It doesn’t seem to impose or learn any patterns. These constraints also help me not mess things up or notice when the AI is making mistakes.

In my Django app, authentication and an admin panel are built-in. I don’t need to rely on the AI to build it for me. This also means that we (the AI and I) can’t mess it up.

I have also decided to move away from React (which I will really miss), but again, its dependency story is too scary for me. I am going with HTMX and server-based UI (something I have been trying to return back to). I’ll tell you why tomorrow.

Dev Stack 2025: Part II – Linux

I have been developing on a Mac full-time since 2013, but I’m rethinking how I do everything. Switching to Linux was the easiest choice.

To me, software development is becoming too dangerous to do on my main machine. Supply chain attacks and agentic AI are both hacking and data destruction vectors and need to be constrained. Given that, I decided to build a machine that was truly like factory equipment. It would only do development on it and give it very limited access.

I wanted it to be a desktop to maximize the power per dollar, and since I don’t do any iOS development anymore, there was no reason not to pick Linux.

Mac laptops are perfect for my usage as a consumer computer user. The battery life and track pad are unmatched. My MacBook Air is easy to transport and powerful enough. But, as a desktop development machine, Macs are good, but not worth the money for me. I decided to try a Framework instead, which I might be able to upgrade as it ages.

When I got it, I tried an Arch variant first, but it was too alien to me, so I reformatted the drive and installed Ubuntu. I spend almost all of my time in an IDE, browser, and terminals, and Ubuntu is familiar enough.

Having not used a Linux desktop before, here’s what struck me:

  1. Installing applications from a .deb or through the AppCenter is surprisingly more fraught than you’d think. It seems easy to install something malicious through typos or untrusted developers in App Center. Say what you want about the App Store, but apps you install must be signed.
  2. Speaking of App Center: its UI flickers quite a lot. Hard to believe they shipped this.
  3. Generally, even though I have never used Ubuntu Desktop, it was intuitive. The default .bashrc was decent.
  4. I like the way the UI looks, and I’m confident that if I didn’t, I could change it. I need that now that my taste and Apple’s are starting to diverge.
  5. I still use a Mac a lot, so getting used to CTRL (on Ubuntu) vs. CMD (on Mac) is a pain.
  6. I was surprised that I need to have a monitor attached to the desktop in order to Remote Desktop to it (by default).

In any case, I set up Tailscale, so using this new desktop remotely from my Mac is easy when I want to work outside of my office.

My next big change was to go back to Django for web application development (away from node/express). I’ll discuss why tomorrow.

Changing my Dev Stack (2025), Part I: Simplify, Simplify

My life used to be easy. I was an iOS developer from 2014-2021. To do that, I just needed to know Objective-C and then Swift. Apple provided a default way to make UI and it was fine.

But, in 2021, when I went independent, I decided to abandon iOS and move to a React/Node stack for web applications (that I wanted to be SPA). I chose React/ReactNative. It was fine, but I have to move on.

The main reason is the complexity. For my application, which is simple, there are an insane amount of dependencies (which are immediate tech debt, IMO). Hello World in Typescript/Node/React will put 36,000 files (at last count) in node_modules. This reality has become a prime target for hackers who are using this as a vector for supply chain attacks. It’s clear that the node community is not prepared for this, so I have to go.

This is a major shift for me, so I am rethinking everything. This was my criteria:

  1. Minimal dependencies
  2. No build for JS or CSS
  3. Security protection against dependencies
  4. Bonus if I already know how to do it

The first, and easiest decision, was that I was going to move all development from my Mac to Linux. I’ll talk about this tomorrow in Part II.

Code Coverage Talk at STARWEST

Last September, I spoke about enhancing code coverage at STARWEST. My talk was based on ideas that I introduced in Metrics that Resist Gaming and some related posts.

The key points are that metrics should be:

  • able drive decisions.
  • combined to make them multi-dimensional.
  • based on leading indicators that will align to lagging indicators.

And then I applied that to code coverage. I combined it with code complexity, the location of recent code changes, analytics, and then I stress tested the covered tests using mutation testing. The idea is that you should care more about coverage when the code is hard to understand, was just changed, or users depend on it more. And since coverage is only half of what you need to do to test (i.e. you also need to assert), mutation testing will find where you have meaningless coverage.

As a bonus fifth enhancement, I talked about making sure you were getting the business results of better testing. For that, I spoke about DORA and specifically the metrics that track failed deployments and the mean time to recovery from that failure.

Algorithmic Code Needs A Lot of Comments

I recently read an online debate between Bob Martin and John Ousterhout about the best way to write (or re-write) the prime number algorithm in The Art of Computer Programming by Donald Knuth. Having read the three implementations, I think Knuth’s original is the best version for this kind of code and the two rewrites lose a lot in translation.

My personal coding style is closer to Ousterhout’s, but that’s for application code. Algorithmic code, like a prime number generator is very different. For most application code, the runtime performance will be fine for anything reasonable, and the most important thing to ensure is that the code is easy to change, because it will change a lot. Algorithmic code rarely changes, and the most likely thing you would do is a total rewrite to a better algorithm.

I have had to maintain a giant codebase of algorithmic code. In the mid to late 2000’s, I worked at Atalasoft, which was a provider of .NET SDKs for Photo and Document Imaging. We had a lot of image processing algorithms written in C/C++.

In the six or so years I was there, this code rarely changed. It was extensively tested with a large database of images to make sure it didn’t when we updated dependencies or the compiler. The main two reasons why we would change this code was to (a) fix an edge case or (b) improve performance.

The most important thing that this code could have that would help is a lot of documentation. It was very unlikely that the coder would know what this code was doing. It probably would have been years since its last change, and unless it was our CTO making the change, there is no way anyone could understand it quickly just from reading the code. We needed this code to run as fast as possible, and so it probably used C performance optimization tricks that obfuscated the code.

Both Ousterhout and Martin rewrote the code in ways that would probably make it slower at the extremes, which is not what you want to do with algorithmic code. Martin’s penchant for decomposition is especially not useful here.

Worse than that, they both made the code much harder to understand by removing most of the documentation. I think they both admitted that they didn’t totally understand the algorithm, so I’m not sure why they think reducing documentation would be a good idea.

To be fair, this code is just not a good candidate to apply either Ousterhout’s or Martin’s techniques, which are more about API design. Knuth described the goal of his programming style this way:

If my claims for the advantages of literate programming have any merit, you should be able to understand the following description more easily than you could have understood the same program when presented in a more conventional way.

In general, I would not like to have to maintain code with Knuth’s style if the code needed to be changed a lot. But, for algorithmic code, like in his books or in an image processing library, it’s perfect.

Make a Programmer, Not a Program

From what I have seen, pure vibe coding isn’t good enough to produce production software that is deployed to the public web. This is hard enough for humans. Even though nearly every major security or outage was caused by people, it’s clear that that’s just because we haven’t been deploying purely vibe coded programs at scale.

But, it’s undeniable that vibe coding is useful, and that it would be great if we could take it all of the way to launch. Until then, it’s up to the non-programming vibe coder to level up and close the gap. Luckily, the same tools they use to make programs can also be used to make them into programmers.

Here’s what I suggest: Try asking for very small updates and then reading just that difference. In Replit, you would go to the git tab and click the last commit to see what changed. Then, read what the agent actually said about what it did. See if you can make a very related change yourself. For example, getting spacing exactly right or experimenting with different colors by updating the code yourself.

Do this to get comfortable reading the diffs and to eventually be able to read the code. The next step would be being able to notice that code is wrong, which is most of what I do these days.

    How to Get Changes Through QA Faster

    In PR Authors Have a lot of Control on PR Idle Time, I made the argument that there is work the author could do before they PR their work that would get a PR review started faster. I followed up in A Good Pull Request Convinces You That it is Correct to show how to make the review faster once it started. The upshot is you do a code review on your own code first and fix problems you find. That work doesn’t take long (an hour?), but shaves off hours and days off the code review.

    The same technique works for QA: Do your own testing and update the issue/bug/story/card in your work database to make it clear what the change was and how you have already tested it (with proof).

    The worst case scenario for a code review and QA are the same: your code has a defect that you should have found. You can do work up-front to make sure this doesn’t happen, and that work is short compared to the wasted time that not doing it will cause.

    I assume that you will test your code before you submit it. Hopefully you do that through automated unit-tests, which should include edge cases. You should go beyond that and anticipate what QA will check.

    Like with code reviews, this extra work takes a couple of hours and potentially saves days of back-and-forth between your testers and you. If you don’t have any ideas of what to test then check with AI chatbots—they are pretty good at this and can even generate the test.

    If you can’t automate the test, then you still need to manually test it when you write it, so it’s a good idea to make some record of this work. For example, for UI code, which is hard to unit test, create a document with before and after screenshots (or make a video showing what you changed).

    These ideas also help with another source of QA feedback—that they don’t even understand what the issue/story/bug is. The way I head that off is by attaching a “Test Plan” document with a description of how to see the change in the application and what specifically was changed. A video works here too.

    When QA finds a problem that I could not have found, then I am relieved. But, when they kick something back because it wasn’t explained well or I made a stupid mistake I could have easily found, I feel guilty that I wasted their time (and mine). I’ve never regretted taking a little time at the end of a task to help it go smoothly through the rest of the process.

    The Infinity-X Programmer

    Forget about the 10-X programmer. I think we’re in a time where AI coding assistants can make you much better than that.

    Even if you think I’m crazy, I don’t think it’s a stretch that some programmers, particularly less experienced ones, will get a big relative boost compared to themselves without AI. Meaning, they could become 10x better using Cursor than they would be if they didn’t use AI at all.

    The norm is less for experienced devs. I think I’m getting about a 2x or 3x improvement for my most AI-amenable tasks. But when I have to do things on projects where I don’t know the language ecosystem as well, it’s much more. So, it’s less about overall skill, and more about familiarity. As long as you know enough to write good prompts, you get more of a multiple the less you know. For example, for my main project, I might save an hour on a 4-hour task, but a junior dev might save days on that same task. Even if I finish it faster this time, they are still going to improve on that same kind of task until we’re about the same.

    But, I also think it’s possible to get very high objective, absolute multipliers against all unassisted programmers with projects that are not even worth trying without the AI assistance.

    I’ve started calling this Infinity-X programming. I’m talking about projects where it would take weeks for a programmer to complete, but no one is sure that it’s worth the time or cost. Using tools like Cursor and Replit, I’ve seen instances where a person with some programming ability (but not enough to program unassisted) do it on the side, working on it for fun just because they want to. They get somewhere fast, and now we might approve more work because we can see the value and it feels tractable. I’ve seen this happen a few times in my network lately.

    It’s not just “non-programmers”. I’m also seeing this among my very experienced programmer colleagues. They are trying very ambitious side-projects that would be way too hard to do alone. They wouldn’t have even tried. But, now, with AI, they can make a lot of progress right away, and that progress spurs them on to do even more.

    Without AI, these bigger projects would be too much of a slog, with too many yak-shaving expeditions, and lots of boring boilerplate and bookkeeping tasks. But, with AI, you get to stay in the zone and have fun, making steady progress the whole way. It makes very big things feel like small things. This is what it feels like to approach infinity.

    The Central Question of a Code Review

    When I prepare code for a pull request, I construct it commit-by-commit in a reading order that convinces you that it is correct. So, when I stage code, I ask myself: “Is the code in this commit obviously correct?” If it’s not, then I probably need to add commits before this one that make the code more clear because I only make explanatory comments on a PR as a last resort.

    A PR comment becomes code in a few steps. Step one is that I make comments in the code instead of in the PR. This is better because now anyone reading the code will understand it better. A PR comment isn’t tied to the code unless you think to check the logs and follow that back to the PR.

    Step two stems from my belief that a random comment explaining a block of code indicates that that code isn’t clear. This is usually something you can fix with a behavior-preserving refactor. Maybe a variable name is unclear or some code should be extracted into a function. The name of that new function should make the intent of the code inside it easier to see.

    But, changing this code might break something, so step three is try to cover this area with unit-tests. When I read a commit with unit tests, I know that all of the cases in the tests are checked, so my job is to think of things that aren’t checked.

    It’s tempting to want to fix things in code you are reading, but if they aren’t clarifying the work at hand, that might be a waste of time. By concentrating my efforts on making the PR easier to review, the code will be merged faster if I fix the code.

    How it Feels to “Program” with AI

    When I type a prompt into the chat pane in Cursor, it is indistinguishable from programming to me. The part where I tap tap tap on the keyboard and code comes on the screen isn’t programming, that’s typing. The part where I use keyboard shortcuts to navigate the IDE isn’t programming either. Both of those parts (the typing and navigating) is being done by a robot when I prompt Cursor, but the programming is still done by me.

    When I look at a ticket in JIRA that says, for example, “add a way to archive a contact” in my React/Node/MySql application, when I estimate, I think

    1. Add an archived field to the contact entity, default to false, set as non-nullable
    2. Generate a migration and run it on my local database
    3. Add DB service functions to archive and unarchive contacts
    4. Write unit tests for those DB service functions
    5. Add GQL mutation functions to archive and unarchive a contact
    6. Add archived to client GQL queries
    7. Add archived to the client-side contact model by running the GQL code generator
    8. Make sure to set up the model’s archived field from the GQL query in Redux
    9. Add a Redux reducer to set the archived field
    10. Add Client-side functions to optimistically update the redux and call the GQL mutation (undoing on error)
    11. Add an “archive”/“unarchive” button on edit on the contact edit dialog (show the one that applies to the contact)
    12. Look at lists that show contacts and decide if they need a way to filter archived contacts out or not

    I can tell you from experience, that I can do steps 1, 3, 4, and 5 with a prompt that has basically what that says and at-mentioning the files that will be updated and that serve as a model (I probably have another entity with an archived field). Step 2 is a yarn script for me that compares the schema in my code to the one in my DB. Steps 6, 7, 8, 9, and 10 would be another prompt, and finally I will do 12 & 13 manually or with completions because I might want to adjust the UI.

    Before Cursor, I still wrote out that list because I like to Build a Progress Bar for My Work that helps me make an estimate, keep on track, and know if I am not going to make it. When I work with Junior devs, I often develop this list with them to communicate what I want done with more details.

    Is this programming? I think so. Instead of TypeScript, I am “programming” in a loosely specified, natural language inspired, custom DSL. I run scripts to generate my migration code from schemas and my client side models from GQL queries, and to me, prompting Cursor is basically the same thing.