Every couple of years in software development, the meta changes. Libraries and frameworks are rotated in and out of popularity, languages evolve and best practices change. These are some of my personal beliefs1 on what the current meta is, and what parts are worth adopting.
When it comes to following best practices, there’s a good quote from The Zen of Python:
Now is better than never.
Although never is often better than *right* now.
It’s a bad idea to never adopt a practice just because you didn’t adopt it from the start, but following the latest trends before they’ve become established is probably a bad idea too. As they say with trees, the best time to plant a tree was 20 years ago. The second best time is now.
These are broken down into 🧑💻 coding, 🤝 culture, 🛡️ security, 🚇 infrastructure, and 🧪 testing.
I’ve given each a 😍, 😍😍, or 😍😍😍 rating based on how valuable they are, along with a comment on where to use the advice. Everything in this list is something I find valuable, though2.
Use statically typed languages over dynamically typed languages
🧑💻 Coding
😍😍😍 Use everywhere
Dynamically typed languages powered much of the web for the first part of the 21st century. Python, PHP, Ruby and JavaScript on the backend, with JavaScript on the frontend. These are still popular choices, but all of these now support some form of type signatures and type checking.
The preference for dynamically typed languages probably stemmed from a dislike for statically typed languages. That dislike probably hails from the languages that introduced many to static types: Java, C, and C++. Java is the best example since it has been used widely in education. Verbose syntax and grammar combined into simple things involved a lot of boilerplate. Java’s verbosity encouraged IDEs to generate a large amount of this boilerplate for users, reducing the overall ratio of code expressing meaningful logic. JavaScript3, on the other hand, would allow users to get away with writing a fairly minimal set of code, at the cost of data control and interfaces. Libraries like JQuery, where $
is used as the main function call4 also reflected the practice of keeping things short.
Likewise, the statically typed languages had a poor deployment story. To deploy a PHP script, all that needed to be done was to replace the source files on the server. To deploy a Java application, you’d need to compile it, deploy the binary, then set the Java server software to restart.
Times have changed, and static typing no longer needs to be complex for developers to write or generate. Even developer tools that fall in the “text editor” category rather than IDEs support features that were previously only really seen in IDEs. This means that users of modern statically typed languages typically get features like type definitions, autocomplete, and powerful type inference in any editor they wish to use.
The benefits of statically typing Python or JavaScript is apparent if you have ever worked in a large legacy codebase in both untyped and statically typed variants. Fewer runtime exceptions due to unforeseen data shapes, better default documentation, and catching errors during development rather than after deployment have helped make TypeScript and typed Python a household name.
The type systems introduced to JavaScript and Python are known as gradually typed, where types can be gradually added to untyped code over time, to add type checking to that specific part of the code. In practice though, I find that it is better to fully commit than to partially commit. Flagging untyped function calls and variables as an error helps actually gain the benefits of a typed language.
Many complaints could be leveraged against these, though. TypeScript’s errors can be long and confusing, with configuration options changing core features of how the compiler behaves. Compiling TypeScript can be slower than deploying JavaScript, though the ability to use a TypeScript compatible runtime alleviates that. Python’s typing options and syntax don’t go far enough, if you ask me. But still these options are a great way forward - write a service in pure JS and pure TS, come back in 5 years and see which one is easier to maintain.
Use automatic formatting
🧑💻 Coding
😍😍😍 Use everywhere
As someone with an Elm and Go background5, it should come no surprise that I love automatic, standardized formatting. Even for two of my own languages (Derw and Gwe), automatic formatting was one of the first things I built. Automatic formatters are different from normal code formatting, in particular:
They are provided as a core feature of the language.
They are strict, with few or no options.
They provide a way for CI to assert whether committed code needs formatting.
In the days before such formatters, developers would have long, pedantic, debates over coding style during pull requests. Programmers are generally a very passionate bunch when it comes to code. Each team over time would build up their own standards and guidelines, but these would be wildly different between companies, or even between teams within companies.
Auto-formatters put an end to these discussions. For each language, there is a singular correct way to format code: and the developer writing code doesn’t need to care as long as their code compiles as the formatter will fix it for them. It results in all code being familiar and comprehensible to the reader, regardless of what team they were on prior.
Prettier (JavaScript, TypeScript) and Black (Python) are two such formatters which have reached popular status in their communities. Set these up in your editors, with a pre-commit hook if necessary, and a check in CI to assert that committed code has been correctly formatted.
No formatter is without faults, but they certainly make sure that more time is spent on the logic and purpose of code.
Use union types
🧑💻 Coding
😍😍😍 Use everywhere
Union types allow the developer to combine several pieces of data under a single data type. Many languages in recent years have them built in, with the most notable example of their usage being Maybe or Optional types. In many older languages, null allowed for developers to represent the presence or the absence of some data. Since null is known as “The Billion Dollar mistake”, many modern languages moved away from having all values be nullable, and instead moved to explicit representation - maybe some data is there, or it’s not, and the programmer must cover both cases. Union types are also commonly used as Result or Either, where they may represent a success or a failure value. A success could be something as complicated as a User type, and a failure could be as simple as a string with an error message. These patterns allow for concise representation of two possible bits of data that could be returned from a function call.
Beyond these examples, union types can be used for scoping and limiting flags, or creating powerful APIs without resorting to permissive arguments. Some languages even allow for literals to be used in a union, which can be very handy for limiting some input to a specific range.
Not all languages support union types though, and in some it’s very clunky. If you’re on Python 3.9 or earlier, it’s well worth upgrading to 3.10 so that you get the newer syntax.
Parse, don’t validate
🧑💻 Coding
😍😍😍 Do everywhere
This is well covered by a blog post here, but it’s been a practice followed in the statically typed functional programming languages for a while. While parsing is the process of constructing a valid data type from a source, validating is the process of checking the structure of a source. The difference is subtle, but imagine that you have a JSON endpoint returning some data about a user. One approach would be to assume the JSON has a particular shape, then check the values the fields contains — this would be validating. The other approach would be to transform the JSON into a data structure, failing if the JSON does not have the right fields — this would be parsing. The real difference between these approaches is that when validating, a value still exists in your program even if it is invalid. Parsing means that if the data is invalid, the developer must handle that case explicitly - there is no way to skip it.
Union types are a great case for parsing, where a value returned from a parsing function could either be a User or an Error.
Parsing helps solve one of my biggest problems with TypeScript: data from the external boundaries aren’t parsed, meaning that when an API changes their data format, runtime errors are abound with all type safety becoming meaningless. Zod is a great way to get around that, though the API is a little less clean than native TypeScript type definitions.
Avoid abstracting too early
🧑💻 Coding
😍😍 Keep in mind while developing
DRY (don’t repeat yourself) has been promoted as a good idea for a while, but I’ve seen many cases of premature optimization. DRY intends to cut down on duplicate effort and logic, by creating a single source that can be used in multiple places. If you’re familiar with the domain of the problem you’re solving, you probably can figure out the large abstractions that need to exist. But in unfamiliar domains, or in smaller parts of familiar domains, the abstractions might not be clear right away. Premature DRY strikes before you have become familiar with all the cases where a function might apply. Perhaps from the onset, it might seem logical to call a particular function from two different places, but you find that the function has different requirements.
For example, consider a site where you have a User type, containing name and title details. A classic example of premature DRY applying would be to create a singular getUserName(user: User)
function, but discover in one area of code, you need to refer to a person as “Dr Surname”, another as “Firstname Surname”, and another one again as “Dr Firstname Surname”. If you had created getUserName
too early, you may fall into the trap of choosing which presentation to use based on some options param (e.g getUserName(user: User, displayFormat: Displayformat)
). There’s nothing particularly wrong with this, but in principle avoiding large numbers of params, of which many are options, is generally a good idea. There are exceptions to that, of course.
A better principle, I find, is to start with the concrete. Still write things in functions for sure, and reuse them. But don’t commit to a single function serving the purpose of multiple uses. If you do still want to do that, I recommend using a union type to represent the options, then have the public API function switch over the option mode and call the actual function implementation. It’s easier to test, and it keeps logical differences separate.
Once you’ve identified a pattern emerging in multiple places, abstract out the type or function to represent the use cases and refactor your code to follow it.
Be aware of monads and functors
🧑💻 Coding
😍 Know the patterns
Both Optional and Result in the union type examples are normally monads, and a great example of where monads are useful. Monads are really a simple concept, but the name and the background to them can be a bit scary. It sounds mathematical, and it stems from there, but it’s nothing to fear. Understanding them will enable a developer to write better APIs.
While a typical reaction to returning some complex data type would be to unwrap it as early as possible, so that all code thereafter just has to deal with the raw unwrapped data, monads allow for data to be contained and manipulated.
Why this is useful might not be apparent, but consider how exception handling works in most languages. There will be some cases where you’d like to handle an exception as soon as it happens, but in a lot of cases you’d actually like to bubble up the error so that the top-level code calling your API is able to handle it. Monads serve the same purpose. A monad can be returned and handled at the appropriate level, without the need for unwrapping the value and recreating it when returning.
There are plenty of posts on what monads or functors are, so I won’t go into that. But explained simply, a monad is simply an API that a data type adheres to. All monads behave the same way, with functions following the same rules as any other monad. This is why terms like monad or functor are popular: they allow a developer to convey “this type behaves in a particular, well defined, way”. Without this, discussions and APIs end up re-implementing the same interfaces, but with slightly different names or contracts. Don’t be afraid to familiarize yourself with what these terms mean, but be aware the technical names can put some developers off. Reading additional papers popular in the Haskell will expose developers to more of these terms unfamiliar outside of the FP community, but understanding them will lead to better programming patterns.
Accept that generative AI is here to stay
🧑💻 Coding
😍 Understand the use cases
It wouldn’t be 2024 without mentioning generative AI. Whether it’s a potential solution to some production problem, a buddy to help writing code, or a content producer, gen AI has many real world uses. And for the most part, it’s ready for them.
Being familiar with what gen AI can do will help developers keep ahead. All the signs point to it having a lasting impact on not only our industry, but across pretty much every industry. It’s getting more and more investment and attention, and is unlikely to go away overnight. Not all things which see a lot of attention end up being impactful, though.
As a developer, you could try out Copilot. It is particularly strong in the most popular languages, and writing a lot of boilerplate that might be beyond editor tooling (e.g test cases). You can think of it as a digital sparring buddy. Personally, I prefer writing my own code, but can appreciate the value it provides.
Prefer integration tests over unit tests
🧪 Testing
😍 Both are best, but if only picking one, go for integration tests
Test driven development encourages writing tests prior to implementation code. This can be very handy for ensuring that the requirements are understood and edge cases are considered. These tests are typically unit tests, each unit testing one specific small function for a set of given arguments. Unit tests are typically quick to run, and avoid the complexity of needing to mock a large part of the system. Tests that go across boundaries, combining the testing of several functions into one, are called integration tests. Integration tests should also be part of a normal test driven development workflow, but unit tests are easier to reach for before writing any code.
Tests often end up neglected in small, fast moving companies. However, tests often allow developers to move faster with more confidence that the changes they make won’t change things that they did not intend to change. Integration tests better fit this purpose over unit tests, as unit tests typically verify small functions which rarely change in definition, and not their interactions.
Many frameworks and tools have emerged for integration tests, such as headless browsers or screenshot testing. The main downside of these is that typically a single integration test takes longer to run than a unit test. A good testing suite runs in seconds, not minutes. Developers should be able to run tests locally, as they develop, rather than only in CI. This isn’t always possible, but it’s the goal to aim for.
Be kind during code reviews
🤝 Culture
😍😍😍 Do every time
Most teams will implement code reviews. Not all teams will use them effectively, though.
Pull requests can be too large to review realistically, meaning that the reviewer ends up either putting off the review, or reviewing the changes poorly. Smaller pull requests with a good description and a link to any issues they solve help the reviewer gain context.
Reviewers also need to be kind. I like to use a labelling system for my reviews:
🐛 for changes that I think are bugs
❓ for questions or clarifications
🎨 for code style improvements (e.g refactors)
Bugs and questions should be addressed before merging, but code style is optional. Emojis help introduce a human, friendly tone to reviews. The goal is to avoid heated debates, and let developers know when it’s okay to merge or not. Often heated debates can devolve into personal attacks or simply people feel like they’re being personally attacked, even if that was not the intention. Code reviews are there to ensure that code committed is of high quality, addresses edge cases, and to help the team evolve and learn. Keep it friendly and educational and the whole team benefits.
Respect a candidate’s time
🤝 Culture
😍😍😍 Do every time
The job market is a little rough right now world wide, with lots of layoffs and a lack of growth. This means that there’s a whole bunch of great candidates out there looking for jobs. What that doesn’t mean, however, is that employers should make potential employees spend vast quantities of time on application processes.
Hiring should aim to replicate work conditions as much as possible. Put the candidate in touch with people they’d actually be working along side. If there’s code involved in an interview, then candidates should be using their own development environment. Every developer builds up their muscle memory for the editor they use daily. These don’t matter too much for writing some code, but editing, refactoring, and testing all suffer when using an unfamiliar environment. Avoid online or web-based editors at all costs.
Using take-homes is an industry standard practice. The idea is to give candidates some task so interviewers can see how they’d go about solving it. I recommend giving a time-limit of 2 hours. If a candidate applies to 10 jobs, then they’d be spending half a working week just on a pre-interview stage. Any more than that is unreasonable, and likely to filter out a wide range of well suited candidates. Don’t consider the take-home to be a realistic example of a production project. Use it as a discussion point. I typically ask the following questions in my interviews after a take-home:
What areas of the code would you want to refactor? Can you refactor them with me?
What would you do if you wanted to add X feature?
Can you identify any particular bugs or security issues?
What would you have done differently?
Good interviews have a back and forth discussion, not an interrogation. Just as the interviewer is trying to get to know the candidate, the reverse is true too. Would you rather work somewhere where your colleagues question every decision you make with a fine comb, or would you rather work at a place where your colleagues have engaged discussions but trust you to write code?
Github classrooms are a great way to distribute a take-home. It’s free, easy to set up, and allows you to easily add your team members to review code.
Whiteboard interviews should be avoided, though a whiteboard being in the room as an aide is very handy. Don’t make candidates write code on a whiteboard, but drawing boxes can help explain architectural considerations.
Avoid personality or IQ tests. Many personality tests have questionable scientific merit6, rarely reflect a programmer’s ability, and can be almost insulting7. I recommend all senior developers to reconsider processes that involve personality tests, but acknowledge that junior developers often don’t have the same luxury. Much like tipping culture, if IQ tests become normalized, then everyone will have to take them to find jobs. Time spent conducting personality tests are better spent having informal interviews where candidates can have a conversation instead. If you’re still not sure about a candidate, reach out to their references who actually personally know the candidate.
Pair or mob program frequently
🤝 Culture
😍😍 Do it on a regular basis if your team appreciates it, but not all the time
Pair programming has become a staple, especially to pass on knowledge and experience from one developer to another. The exchange of experience happens both ways, but is particularly powerful when one of the pair is deeply familiar with a stack or library.
Mob programming is much the same, but in a larger group. As the group is larger, then more rules should be followed to ensure it’s a productive environment for everyone. Pair programming should have rules established too, but it becomes more important as the concept scales up.
Some good rules to follow:
Set time boundaries for rotating who is typing (either fixed time, per test case, or per feature).
No phones / notifications unless it’s critical.
Take frequent breaks, either to go do something else, or just to chat.
Don’t always pair with the same person - instead try to pair with people you can learn most or share most knowledge with.
Everyone should use their own environment when driving (it’s a great way to pick up productivity tips)
Some work is best done solo, and some developers prefer working alone. This is fine too!
In Norway, we use the term sparring to refer to when two people bounce ideas off of each other. This is much like pair programming, but often done as informal conversations. It’s a good alternative for people who don’t like coding in front of others.
Pick a git commit format and stick to it
🤝 Culture
😍 Do it across all your repos, and standardize across them
It doesn’t particularly matter what shape your git commits take, what is important that it’s consistent across all your projects and all your teams. Conventional Commits or Semantic Commit Messages are pretty good defaults. You’ll want to be able to search through commit messages to find when particular features or fixes were made. A standardized format also helps with generating changelogs. If you’re not a fan of the feat, fix, chore format, you can use emojis. The labelling and grouping is more important than what the particular labels are or how they look. Fewer labels rather than more is probably easier for the developers to use correctly, though. It helps a bunch if your Git commits reference your issue tracker too, as context in the form of bug reports or screenshots often aid when trying to debug if a change is relevant to a new bug cropping up.
If you have an existing repo, then you may be put off by the fact that suddenly years of commits don’t fit the format you choose, but conformity has to start somewhere.
If you’re looking to bulk modify a bunch of commit messages to fit a new format or a migrated issue tracker, there are various ways to do that. git-filter-repo is a decent option for avoiding complex Git commands.
Use Dependabot and friends for dependency maintenance
🛡️ Security
😍😍 Use in production codebases (including libraries or frameworks with users)
It’s easy to fall behind on upgrading packages in your main codebase. It is harder still to remember to upgrade dependencies for older services. In the old days, teams may have had dedicated ops engineers to perform or track updates, but with the move to dev-ops, the burden lies on the developers to do so. Updating dependencies can be crucially important, with security fixes or bugs being fixed. But it can also take a lot of time and effort, and code quickly gets out of date.
Some teams may approach this through routine: once a month, a developer goes and checks each repository for any updates and upgrades what is needed. A better approach would be to use tooling such as Dependabot. They can provide either alerts or automatically created pull requests for a wide range of languages. Finding the right configuration options to reduce noise vs signal can be important though, especially in JavaScript where it is not uncommon to have thousands of dependencies per project - with the average open source repo in 2020 having close to 700. Dependency counts are often higher on work projects, as they combine several libraries and frameworks together.
Dependabot is already seeing growing adoption, where in 2023, 60% more pull requests created by it were merged on open source projects. Getting started is simple, finding a rhythm that fits your workflow is important to avoid feeling nagged.
Write infrastructure-as-code
🚇Infrastructure
😍 Use beyond prototyping
Tools like Puppet, Chef and Ansible long reigned for those aiming to make their infrastructure repeatable. Often the infrastructure would be created manually by hand, through web UIs or command line tools, but Puppet and friends would help to make the provisioning and deployment of server stacks much simpler. There’s the old mantra “If you’re going to do something more than twice, automate it”. These tools would help with that.
An approach to infrastructure early in a project’s lifetime is to build manually what is needed, then scale as you go. This concept works fine during a test period, but there’s plenty that could go wrong, particularly in a startup. I recall one instance of somewhere I worked accidentally deleting the databases, with no way to recreate the data or the schema as they were manually created and adjusted during development. If your product has had any significant time investment into the infrastructure, it’s probably worth making it repeatable. Not only does this help with recreating what you currently have, but it also provides a way to scale services horizontally or move to different providers.
In recent years, options such as Terraform8 and CloudFormation have become massively popular over either manual configuration or solely using tools like Puppet. Terraform approaches the problem of infrastructure in a very different way, where instead of running scripts or modifying files, infrastructure is instead described declarative and Terraform handles all the modifications for the user. CloudFormation is similar, though I would personally recommend Terraform over CloudFormation to help avoid vendor lock-in. The developer experience with CloudFormation is a little weak.
Use platforms that allow developers to focus on the code
🚇 Infrastructure
😍😍 Follow in small companies or teams
As a practice, putting developers in charge of ops has become quite costly. While a better understanding of infrastructure helps developers solve problems in new or better ways, maintaining them can cost a lot of valuable developer hours. Several options such as Heroku or Convox have become popular as a way to just focus on the code to be deployed, rather than the deployment process itself.
Developers whose roles focus on code should be able to focus on code. It is not a bad thing to have a specialism. Either commit to having full time ops, who can maintain stacks appropriately, or develop on platforms which require minimal ops input.
Use queues as data sources
🚇 Infrastructure
😍😍 Use when your problem is event-driven
Several cloud providers offer some form of queues. Queues are a great way to deal with event-driven systems, often with some configurable properties such as dead-letter queues, ordering, and throughput. Services can then be set up to poll the queue and process the messages. Choosing this structure over a traditional database and server architecture allows for better horizontal scaling, and employing serverless. Many of the guidelines for a regular API apply to queues, such as having a documented schema for your messages, though messages may be smaller, depending on the provider, than you would like.
In a media company like Schibsted9, SQS has a natural fit for working with articles. Each article update is a message, processed in order, then notifications are sent to other services, including phone notifications.
Did you find this post insightful? Subscribe to hear more, as the next topic to cover will be specific language runtimes, frameworks and libraries!
My role is to be a tech enabler as part of the CTO Office at Schibsted News Media. I’ve been a developer of various levels and roles for a good while, and now my job is to help facilitate our developers and teams in doing what they do best. It may seem like these are instructions, but rather these are my personal recommendations. They’re things I’d follow for writing code myself, and things I’d bring up in conversations with fellow programmers.
This isn’t an exhaustive list, and there’s probably a lot of things that I think are good ideas that aren’t included. Each advice is a recommendation, but not a requirement at all. Use the post to inspire you to write your own list! I’d also love to hear some counter opinions that disagree with me.
JavaScript probably was also helped along by being the default frontend language for a long time, but the same can’t be said for Python, PHP, Perl, Ruby, etc. PHP even today powers something like 70% of the web, though I suspect a large part of that is down to tools like Wordpress having a dominate position.
The $ is the entry point into making everything JQuery, so it’s not like every function has a short name. But the equivalent in Java would be some very long names instead.
Check out my blog post covering my love of programming languages on my Derw blog for more backstory!
I’m a little biased, having worked with both data collection and analysis of such tools, and did my fair share of reading up on how they worked. Myers-Briggs in particular is one to avoid.
Depending on your market, most developers either have a bunch of formal education (bachelors, masters, PhD), or a bunch of experience programming. Do you really gain much from figuring out how quickly they can solve basic numeric equations or fit shapes into holes?
OpenTofu is a fork of Terraform which might be relevant for companies who care about the changes made to the licensing of Terraform.
We’re a media company responsible for the biggest papers in Norway and Sweden. So, lots of news articles every day.
Pair programming nonsense should be removed. It is not nice to impose yourself on other people unless absolutely necessary. Moreover, it's what Copilot is for now.
OpenTofu is the open fork of Terraform.