Thursday, October 29, 2026

Little Things

There are a bunch of little things -- habits -- that I’ve learned over the years that tend to make coding smoother and the results much better.

The primary one is to do as much as possible in advance of the actual coding. Make sure the environment is set up and working, including disks, peripherals, and furniture. Lay a good, solid foundation for things like logging, errors, config, secrets, and security. Get all of that out of the way first, not later as it is needed. When you sit down to write the core code, the last thing you want is distractions, so preparing your working environment to be able to get into that state of higher focus and stay there for longer is a key to being able to do a lot more things a whole lot faster.

The second one is to leave no stone unturned. That is, if you have questions about what or why you are coding, go answer those questions now. The answers don’t get better if you wait, and the lack of them always threatens to turn your labours into pure make-work. If you don’t know, then find out. Don’t assume, ignore, or procrastinate. Find out now.

Another one is to use way too many functions. Way too many. I use functions as a way to name clumps of code. It helps a lot. Even though there are a lot of functions, they shouldn’t end up with crazy or obfuscated names. Generally, they are just some verb as it operates on some input nouns. One word is preferable; two is acceptable. More than that is suspicious. Anything cryptic is offensive.

Functions, instead of comments, as a way to document what you are doing makes it way easier to review the code later and convince yourself that it really is doing what you thought it was doing. Also, they act as the primitives of decomposition. You break down the problem as far as you can put it into a bunch of small functions, but no further. An added bonus is that they make it easier to refactor. They are a powerful construct; use them wisely.

On that note, splitting out the pre-conditions and the core logic is big too. The first part of any function is there just to make sure the second part cannot get messed up. So, get all the asserts, error handling, little calcs, etc. out of the way at the top. Put every piece of data you will need later into its own unique variable; do not be shy. Similarly, use the language to clump stuff that must stay together all together. Clumping stuff saves on a lot of stupid typos.

Don’t waste time on makework, but also don’t be afraid to radically refactor some large chunk of code or a bad feature if it turns out that you were mistaken. If the logic is upside down, for example, and you persist rather than byte the bullet and correct it right now, it is just a time vacuum.

Make everything readable; spend effort on those tiny details. Partly because it shows you care, but also because those pesky things will hide lurking problems. Expect to have to revisit your own code in a few months, when you are too busy with other things; try to make that moment a lot easier.

Be terrified of names. They shouldn’t be robotic; there is no mindless convention for creating them. You really have to go out and find out what the parts are actually called, both technically and in the domain. Misnaming something is a magnet for bugs. If the poor name propagates all over the place, it can cause grief for decades. Give it the right name; put a lot of thought and effort into searching out that right name. Don’t be creative; that is usually just a failed shortcut.

Labour over your loops. Conditional logic is messy, but looping can be bloat. Don’t loop if you don’t have to; don’t bury the loops even if you think it's pretty. Split them out; you should be able to justify each and every one of them. Get rid of them whenever you can. Looping is where every performance problem begins and ends. Loops are not your friends; they are just the cost of doing business. Respect their power to make your evening awful.

Pull back. If it feels like you shouldn’t be spending 50 hours grinding out boilerplate, it is because you don’t need to be spending 50 hours grinding out boilerplate. There is a better way; you just have to calm down and find it. It’s exactly these types of moments where you learn to properly generalize or abstract stuff. The code would have been so similar that it is painful to type it out, but there is a way to get the same results, just smarter. Some techniques are easy, some take years to master, but all of them will keep you out of trouble. So, if it feels like you shouldn’t continue down this ugly brute force path, then don’t. Let that instinct push you into doing a search for a better way. It is out there.

Don’t be clever, be smart. I know’s tempting to find some clever little bit of syntactic sugar that can save a few characters here or there, but instead of being smart, it’s just a landmine. You or someone else will accidentally stumble over it and blow off their leg, usually in the middle of a production disaster or when there are -200 hours left on the clock. It almost always pays to be explicit, just not when it is tedious boilerplate. But otherwise, type it out in full. Eat that little expense, because later you will appreciate that you did.

The most important thing to understand is that although the code is not the system, the code itself is super important because it is your strongest way of controlling exactly what the system does, and you always need to control that to match everyone else’s expectations. It’s universally true that as the size of the code grows, the style, formatting, organization, consistency, and details get more and more important. A tiny demo can be messy, but a massive mission-critical system cannot. To be effective, you must be certain, and to be certain, you have to learn to be precise. That way, when everyone’s back is against the wall and you have to dive in and save the ship, you can go in and do exactly that, with no negative side effects. Perfect code is the dream, but the ability to perfect code quickly and reliably is good too. Don’t set yourself up to fail.

Thursday, October 22, 2026

Tactics vs Strategy

I guess I didn’t understand it when I was younger. Back then, doing things was just doing things. You decide you were going to do something, then you go about doing it. Simple.

But over the years, I was told otherwise. For instance, if you decide you are going to do something big that is relatively far out into the future, that is quite different than making an impulsive decision right now. E.g., deciding to go to University versus deciding to go out on the town tonight.

Two decisions, but very, very different.

When you set a goalpost for yourself far out into the future, that is basically a strategy. Since it’s so far away and often quite vague, it is nothing more than a direction you want to head. It’s essentially proactive; you’re looking trying to get ahead of the game.

When you make a decision right now, based on the immediate circumstances, that is tactical. It is reactive; you were forced into making that decision, and even if you choose not to decide, it is still a choice.

We see that in software development all of the time. Someone decided to build a system; that is the direction and the strategy. Someone else insists that a quirky feature get shoved in at the last minute; doing that work to appease them is tactical.

A development project without a direction is essentially a random walk. As people make suggestions about what to add to the codebase, if there is no way to evaluate them, then everything gets added. Eventually, the different random features conflict with each other. It is a mess. For example, you’re building a code editor, but someone wants it to play audio files. Why?

In that sense, having a direction carefully bounds the work, making it far easier to distinguish between good suggestions and bad ones. Someone once said something about how having a long-term plan allows you to make better short-term choices; that seems to be universally true.

Software development, however, is a little more complex. It is often a multidimensional tug of war, with lots of ropes and all sorts of people trying to constantly pull the work in only their direction. Which means that in order to not upset this balance, you frequently make tactical compromises.

For instance, there is some really good persistence technology that you know will work as expected, but for the initial release you bend to a request to dump in some trendy, rather flaky, tech instead. You know it won’t work as expected, but you lack enough control over the situation in order to stave off the make-work of using an inappropriate component.

What that implies is that many, if not most, of the tactical choices you end up making in order to keep everyone happy and keep the funding coming in are headed straight into technical debt. They are things that you must do now, but will also have to fix later, if the codebase makes it that far. If they are not fixed later, then it will hamper the lifespan of the code, cutting its utility short.

And this is the game in steering large development projects. You need to carefully set that strategy while getting bounced around by tactical issues. For those immediate problems, you either make good decisions in line with your strategy, or you make bad ones that you will remember to revisit and correct later. If you step back and see software development in that light, it gets a little easier to make better choices.

For instance, you’re tasked with building something significant, so you know where you are headed now. But the environment has lots of constraints and politics, so you know you need to weave around all of this. You don’t lose sight of that goal while you are forced to zigzag all over the place.

As the work progresses, you learn how to batch up some of the tech debt cleanup directly with new feature development. So, there was some older part of the system that you were forced to do poorly in the early days, but now, if you extend the scope a little on some new functionality, it will end up covering that mistake. From the earlier example, you put in some less-than-ideal persistence technology, but now you have upcoming work that needs a much larger schema. So, you wrap that new work along with switching the database technology as well. Kill two birds with one stone.

I used to describe this as alternating between short-term and long-term choices, but it is a lot more nuanced. You’re really keeping track of the bad tactical decisions and attempting to fix them along with newer but related work.

That works well when you do a non-destructive refactoring first, for instance, on some code before you extend it. The first work is pure cleanup, correcting earlier mistakes, while the second is moving forward with enhancements. But the second part is now way easier, because the pain was removed with the first effort. What should you refactor? The painful parts. Putting that into perspective, you did a strategic refactoring to undo poor tactical choices in order to smooth out some more tactical work.

Note that this does not only apply to the code. Basically, it is everything else too.

To get going quickly, you choose not to dive too deeply into understanding the domain for the project. That was a tactical choice. But if you add lots of domain knowledge to your resume, a lot more jobs open up later in your career, which is a strategic consequence. Technical knowledge is good, but lots of it goes stale quickly, whereas domain knowledge is more valuable in the long run. You can keep returning to it over the decades.

To expedite a project, people did an ultra-light job of dealing with the analysis. Again, a tactical choice. Later, though, the missing and incorrect information drives a lot of ‘unexpected’ changes, which delay the release, which was a more strategic goal. Fighting or arguing over the mistakes, another tactical choice, further pushes out the dates, making the project look strategically weak and more likely to get cancelled.

Or maybe there is demand to get the code out on a specific date. Again, very tactical. To make that window, lots of work gets cut, like carefully checking and editing the code, and creating a lot of tests. So, both the code quality and the QA fall short of what was actually needed, strategically. But the tactical reaction to the tsunami of operational bugs is to rush through fixing them. That burns everyone out, and the bugs just cascade or move around, since the quality of the fixes is no difference than the original coding; rushed. The strategic choice is to revisit that poor work, but raise the bar this time. Enhance the quality; do much better testing. The tactical twist is to head towards the buggiest code first, but strategically everything is suspect.

If you frame everything this way, it really helps. Without some strategy, you won’t go anywhere, just around in circles. So the goal is always to get yourself closer to the end, but the path there is often quite twisty. If you have to do something, you can decide whether it helps strategically, is useless, or will need to be revisited later. If you just make everyone happy, but get zero movement toward the goal, you lose. If you do too few strategic things, you lose. If it becomes a mess because of too many poor tactical choices, you lose. If you go nowhere because of too much make-work, you lose. You need to do as much of the stuff as you can in order to keep up the momentum and not be a mess.

It’s also worth noting that the goal should always be, at least, solving the user’s problems. If it’s just adding some feature, or hitting a release date, or playing with some technology, those are far too short-term to help. The goal has to be far enough away that it drives most of the tactical choices in a direction. It’s also worth noting that you probably won't be there if and when the project crosses the goal line, and that is fine. Your contribution was to head it off in a good direction; if it gets derailed long after you have left, that is unfortunate, but not your fault. The corollary to that is that if you join an existing project, then the most important thing to learn is the current strategy, and the worst tactical choice you can make is to ignore it or bend it way off into another direction. Strategy changes do happen, but they are slow-moving beasts, since they effectively reweight all of the previous tactical choices. You can’t start writing an accounting system, then switch to it being a video game, or vice versa. The first part is turned into make-work by the shift to the second goal.

In most cases, picking a strategy isn’t hard. But picking it and communicating it to everyone is a key part of leadership, which can be tricky. On the technical side, it is the dev lead's responsibility. If the team is headed one way and a programmer is going off in a different direction, their work now effectively has no strategy; it’s just random. So, it’s critical that everyone adopts the same strategy in order to work together. This means that if you are young or new to a team, even if you have ideas or preferences, you have to set those aside in order to effectively contribute. So, mostly, except for very early greenfield work, we don’t have to come up with a strategy; rather, we have to sort through the noise and figure out what it is. In many cases, it is actually ancient, but that is a good thing rather than a bad one.

Thursday, October 15, 2026

Synchronization

If you are building a software system, then collecting data directly from the users isn’t too hard.

You need an interface, usually a GUI, that lets them add or edit stuff. 

If you modelled the data reasonably, the screen presentations are mappable back to the database format, so you just need a bidirectional way to go between them.

If you need to decorate that interface with some relatively static data, it doesn’t get much harder. You manually get the data from somewhere, keeping track of how often it gets updated. If changes are occasional or a little more frequent, you can put that data into a config file, then maybe try to add in some update automation or screens to make updating it easier.

The game gets a lot harder, though, when you split the program in two with a client/server type arrangement. If the communication is super reliable, it's just a little extra error handling to support rainy days. If the communication is not reliable, then there are a series of hard trade-offs you need to make when handling failures.

In some cases, if the front can’t reach the back, it can effectively freeze. But in some cases, it needs to continue on as normal, then carefully resync later when the backend is available again. What it shouldn’t do is show the user old, stale data; if changes in the backend went unnoticed, that just causes chaos. It also shouldn’t clobber new data that was changed in the backend without being updated.

This gets super complicated if there are lots of users and they are changing stuff all of the time. And bumps up to a huge problem if there are lots of users and lots of backends. Keeping that type of arrangement in sync is right at the edge of our knowledge.

Internally, you want to try and wrap it somehow. That is, you have user data, static data, and remote data. You put that in a layer underneath any of the computations and presentations.

The different types of data are polymorphic, but you wire in the worst-case error handling at the top for everything. You do that because data accessibility problems are essentially interface problems, not data ones. That is, the interface asks for some data, but it is told to wait. Or maybe it is told to drop what it asked for earlier and update it with something newer. The problem isn’t reading data; rather, it is keeping a small window of data in the client synchronized with some other source of data, which means that the wiring has to include any and all of the means to detect that it is out of sync and also to fix the circumstances, as well as support two-directional communication.

So we can play with the idea that the client sends an event to ‘ask’ for data; the server sends an event to ‘show’ a specific version of that data.

If you’re going to go right over the edge, then you might wire up a client as sets of widgets that send ‘ask’ events for specific data entries. Later they get ‘show’ events and respond. At some point, they can also send ‘update’ events for changes to that data. They do this on all ‘sets’ of interdependent data rather than individual fields.

Then, we get a scenario where two clients send a bunch of ‘ask’ events and get back ‘show’ events. The users change stuff; the first one to hit Save gets updated, but then the other client receives an ‘update’ event. For each widget on their screen with modified data, it can show both the new value and the changed value on the screen and ask the second user to pick, or back off.

We get another scenario where the user grabbed the data via a bunch of ‘asks’ followed by ‘shows’. Then the server went down. So on Save, they get some visible indication that the ‘update’ was sent, but the code never gets the ‘show’ back again. The user is notified that their changes are pending and not saved. Again, it can ask the user to address the situation. It can even provide a means to temporarily hold their changes until later.

In this way, we can reframe the problem not in terms of logic to write, but rather in terms of just keeping a small pool of data synchronized with a much larger pool. This type of abstraction is data-driven and is far easier to understand than lots of twisted nested error handling dumped all over the code. You can explore the essence of a problem and find simple machinery in order to mitigate it.

We can even extend this idea to really difficult scenarios. The program displays integrated data from a huge number of different sources. The widgets only display derived data; underneath this, the derivation does a series of ‘asks’, then does recomputes based on the incoming ‘shows’. It pumps back a ‘show’ to the above widget. For some sources, using the very last ‘show’ that is cached is allowable; for some, it is not. Daily calcs, for example, can’t use day-old stuff, but weekly calcs can use stuff up to six days old. We wire in that data retention config at the source, and then everybody on top can only do the right thing. It can’t be ignored; it can’t be overridden. If the data far below, buried in some deep derived data, won’t update, then nobody on top will either.

That type of strict handling is necessary because different people will each handle the same problem in a different way. That freedom begets chaos, which ultimately confuses the users, diminishing the value of the code, not enhancing it. It would be better to be consistently wrong than to have a random mix of correctness. At least then it could be fixed quickly in just one place.

The idea, though, is that we can step back to look at the way the data flows around, or doesn’t. If we change our perspective away from coding and towards just keeping different things in sync with each other, it becomes a lot easier to solve this as a general problem, rather than keep specifically solving it over and over again for each frustrating little instance. Good abstractions aren’t special; they are just stronger ways to twist your perspective a little when a bunch of related problems are causing similar trouble, in order to do a better job solving them all at once. We just need to dig down to the fundamental problems, not the symptoms on top.