Four Software Development Principles I Learned the Hard Way

I recently designed and wrote a huge service, and last month (finally) launched it. During the design and implementation process, I found that a number of patterns, which I'll outline below, kept popping up in a variety of scenarios.

These patterns are so consistent that I would venture to guess that knowledge of at least one of them will be relevant to any reader of the project they are currently working on. But even if they cannot be directly applied to what you are currently working on, I hope these principles will serve as useful food for thought, as well as a basis for comments and objections, which you are free to leave below the article.

One thing I'd like to point out here is that of course there is a time and place for each of these principles. As with everything, it's important to consider the nuances. I tend to hold these conclusions in general because I see from my experience reviewing code and documentation that people often take the opposite course of action as the default.


1. Stick to one source of truth

If there are two sources of truth, then one of them is probably misleading. If it is not, then only for now.

The point is this: if you're trying to determine the same state in two places within a single service… don't bother. A better solution is to just reference the same state wherever possible. For example, if you were supporting a frontend app that fetches a bank balance from the server, I'd always want to fetch the balance from the server – I've seen enough sync bugs in my time. If that value is used to calculate some other balance, such as “available to spend” versus “total” (say, some banks require a minimum balance to be left in an account), then the available to spend balance should be fetched in real time, not stored separately. Otherwise, you'd have to update both balances for every transaction.

In general, if you have some piece of data that is derived from another value, you should calculate it, not store it. Storing this kind of data leads to desynchronization bugs. Yes, I understand that this is not always possible. There are other factors at play, such as the cost of such calculations. Ultimately, it comes down to what is most important to you.

2. Yes, please repeat yourself.

We have all heard about the DRY (Don't Repeat Yourself) principle, and now I bring to your attention the PRY (Please Repeat Yourself) principle.

More often than I'd like, I see code that more or less resembles what I'm looking for being abstracted into a class that can be used in other projects. The problem here is that this “reusable” class is first given a method, then a special constructor, then a bunch more methods, until it ends up being a huge Frankenstein of code intended for a bunch of different purposes, so that the original purpose of abstraction is lost.

A pentagon may look like a hexagon, but there are enough differences between them to consider them two completely different shapes.

I'm not without sin myself – sometimes I spent a lot of time trying to make some piece of code reusable, when I could have just duplicated some parts, and nothing terrible would have happened. Yes, I would have had to write more tests and it would not have satisfied my craving for refactoring, but such is life.

3. Don't get carried away with mocks

Mocks. I love them and I hate them. My favorite short quote from a Reddit thread on the topic is: “With mocks, we increase the ease of testing at the expense of reliability.”

Mocks are great when you need to write unit tests to quickly test something, but you don't want to mess with production-level code. Mocks are less great when something breaks in production because it turns out that something went wrong with something you hastily threw together deep in the stack, even if those deep places are being worked on by another team. It doesn't matter: your service is broken, so you have to fix it.

Writing tests is a tricky business. The line between unit and integration tests is much blurrier than it might seem. Where you can use mocks and where you shouldn't is a matter of subjective assessment.

It's much more satisfying to discover unexpected things during development than in production. The longer I write software, the more I tend to stay away from mocks whenever possible. It's worth it to have slightly more cumbersome tests if the gain in reliability is significant. If mocks are really necessary and a code reviewer insists on them, I'll write more tests (maybe even too many) rather than skip some. Even if I can't use a real dependency in a test, I'll try to find other options before resorting to mocks – for example, a local server.

There are some useful comments on this in the 2013 article. Testing the Toilet from Google. According to them, excessive use of mocks leads to the following:

  • Tests become more difficult to understand because in addition to the production code, there is also additional code that needs to be understood.
  • Test support becomes more complex because the mock must be configured to behave as expected, which means implementation details creep into the test.
  • Tests in general provide fewer guarantees, since the correctness of a program can now only be guaranteed if the mock works exactly the same as the real implementation (which is far from certain, as synchronization between them is often broken).

4. Keep mutable states to a minimum

Computers are very fast. A very popular approach in the optimization race is end-to-end caching with instant storage in the database. I think this is the end point that most successful products and services reach. Of course, most services still require some state, but it is important to figure out what is really necessary in terms of storage and what can be retrieved in real time.

I found that in the first version of a product, there are significant benefits to reducing mutable states to the absolute minimum possible. It speeds up development – you don't have to worry about data synchronization, data inconsistencies, and outdated states. It also allows you to develop functionality in stages, rather than all at once. Computers are so fast now that doing a few extra calculations is not a problem at all. Since machines are supposed to take our place soon, let them solve a few extra problems.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *