In the 1960s, Professor Friedrich L. Bauer developed the first imperative computer programming languages. Although few people know of ALGOL and ALGOL 60 today, their key principles remain visible in modern programming languages like Java, Python, and Ruby.
Many software engineering best practices were formulated decades ago but underwent several iterations to meet modern market demands. Countless new methodologies, frameworks, and toolkits have emerged as digitization increased speed.
So, what sets the best-performing software engineering teams apart from the rest? You’ll get the answers from this post.
Software Development Best Practices For Shipping Better Products in 2024
Globally, spending on IT will increase by 8% this year to $5.1 trillion. A good portion of these budgets will likely go towards new product development or further feature maturation. If you too have ambitious software development plans for this year, this refresher on software development standards and best practices is for you.
1. Always Go for the Simplest Solution
Most software works best when its architecture and code are simple. So, always go for the simplest thing that could work.
Simple systems face fewer dependency issues, are more stable, and are easier to maintain. You’re also less likely to accumulate technical debt, which accounts for 40% of IT balance sheets in many organizations.
When implementing a new feature, consider whether the simplest solution could work. Build the base, run a unit test to validate your idea, and if it works, that's a great result. Likewise, if you’re refactoring an application, use the simplest approach. You can always further improve it during the next iteration.
Not sure what “simple” would mean in your case? Apply the Curly's Law:
“A variable should mean one thing, and one thing only. It should not mean one thing in one circumstance, and carry a different value from a different domain some other time. It should not mean two things at once. It must not be both a floor polish and a dessert topping. It should mean One Thing, and should mean it all of the time”.
In other words: Each part of the code should have one clearly defined purpose. It also supports two other important principles of modern software engineering: Don’t Repeat Yourself (DRY) and Once and Only Once.
2. Keep Your Code DRY
In a now classic book, The Pragmatic Programmer, Andy Hunt, and Dave Thomas formulated the Don't Repeat Yourself (DRY) principle:
“DRY is about the duplication of knowledge, of intent. It’s about expressing the same thing in two different places, possibly in two different ways”.
Duplication undermines system simplicity and creates ambiguity for others. It leads to poor factoring and creates logical contradictions. The source code should contain each significant piece of functionality in only one place. When different pieces of code perform similar functions, it’s generally a good idea to combine them into one by abstracting out the differences between them.
To maintain the above principle, effective software engineering teams (like Svitla!) maintain shared knowledge (metadata, documentation, tests, etc) to minimize duplication and maintain unified coding standards.
A subset principle of DRY is Once And Only Once.
Similar to DRY, it encourages developers to ensure that every declaration of system behavior occurs once and only once. When reviewing or refactoring code, try eliminating duplicate declarations of behavior by merging them or replacing multiple similar implementations with a unifying abstraction.
Engineering teams produce higher-quality and more stable systems by applying these fundamental software development best practices.
3. Stive for Connascence
As a metric, connascence describes different levels and dimensions of coupling in software. It was proposed by Meilir Page-Jones, a veteran software engineer and methodologist, to monitor the long-term impact of produced code on system flexibility.
When one component requires a change in the other, they are connascent. Lower connascence equals higher code quality as it reduces the cost and complexity of making changes to software systems.
This software development metric allows the team to compare different types of dependencies in code across 3 separate axes: strength, degree, and locality. Thus, they can make better engineering decisions about allowing certain types of coupling or refactoring of the code. Here’s how different forms of connascence impact code quality:
Static forms of connascence, like name, type, and meaning, indicate a degree to which two components must agree on the formats they use for data exchanges. Positional connascence pertains to consistency in data structures between components, while algorithmic covers the logic used in a particular operation.
Dynamic forms of connascence cover runtime dependencies and interactions between different components. Execution and timing connascence cover the order and the timing of events that occur during runtime. Value connascence denotes the degree of agreement on constant values or literals across components. A component's identity connascence can be measured by its reliance on a global or shared identity.
Overall, low connascence promotes modular system design like microservices, with more independent components, which can be built, tested, and maintained in isolation. Because components with low connascence have fewer dependencies and are more versatile, they can be reused in different contexts and adapted to new requirements, increasing development velocity. Modular systems are also more scalable thanks to code reuse and limited propagation of errors.
4. Build For Maintenance
Poor software quality cost the US economy over $2.41 trillion in 2022, while the accumulated software technical debt (TD) increased to $1.52 trillion.
Technical debt is a grand sum of poor decisions and actions software developers take during the product's earlier stages. Unnecessary system complexity, high execution connascence, extensive duplication — all of these add up.
Good code is easy to read and debug. It’s self-explanatory and doesn’t make another developer second-guess your judgment. Junior developers can also learn from it.
To produce more maintainable code, apply the principle of least astonishment (POLA):
“The results of every operation should be obvious, consistent, and predictable, based upon the name of the operation and other commit comments.”
Every code element should behave exactly as its syntax suggests, with no surprises. Ideally, your code should be self-explainable. Another developer can review it without opening extra documentation.
That said, writing self-explainable code isn’t an easy task. Joel Spolsky, former Microsoft engineer and the creator of the Trello app, openly states that:
“It's harder to read code than to write it. This is why code reuse is so hard. [People on your team] write their function because it’s easier and more fun than figuring out how the old function works”.
This line of thinking creates technical debt and system performance issues. The old code has already been proved to work, tested, and fixed to serve the purpose. “When you throw away code and start from scratch, you are throwing away all that knowledge. All those collected bug fixes. Years of programming work”, according to Spolsky.
The bottom line: Use existing code as a foundation to build upon and improve. But leave the door open for some experiments, too.
Usually, when the code doesn’t do what it was designed for, you have a bug. But sometimes, the produced code has some unplanned properties. For example, it handles an odd input even though you didn’t anticipate receiving one.
In other words, there’s often a difference between what the code was intended to do and what it does. These cases must be well-described and documented so other developers don’t get stumped when working with your code.
5. Test Code Frequently
Like maintenance, software testing isn’t typically the programmers’ favorite task. Without thorough quality assurance (QA), no one can release quality software into production. Most software testing today is automated, giving developers even fewer excuses not to run frequent unit tests.
A good unit software test follows the FIRST principle — it’s Fast, Isolated, Repeatable, Self-validating, and Timely. With a modern QA automation framework, teams can run unit tests in seconds for an array of new code commits.
A good test method relies on AAAs for isolation: Arrange, Act, and Assert.
- Arrange: Unit tests should test only one change in state at a time. We want our code to start in the initial state. That's what the arrange section is for.
- Act section invokes a particular testing method.
- Assert section produces the resulting state or an outcome we expected from the tested code. If we get the expected result, the unit test is passed. If not, you’ll have to start over.
Unit tests, however, are just the base of the Test Pyramid, including service tests (integration, API, etc.) and UI tests.
Unit tests verify how individual system components behave. Integration tests help validate the behavior of the system as a whole. Most systems consist of layers — storage, processing, view, etc. Integration tests help validate the exchanges between these, while API testing helps better understand how the integrations are structured: Is the API too complex? Does it make too many calls to other systems?
In each case, create simple tests to use in a continuous integration (CI) pipeline and run from the command line.
6. Version Control is a Pillar for Frequent Deployments
A version control system (VCS) like Git, Mercurial, or Subversion provides a complete history of changes made to the codebase. VCSs provide a better team collaboration space, improve the traceability of code changes, and enable parallel development, among other advantages.
Teams can work on different product features or bugs in separate code repositories (repos) and merge new repos into the main codebase once they’re tested and approved for deployment. Such parallelization allows teams to develop new software components faster without worrying about change issues. Code repos are isolated, meaning that early experiments and possible mishaps won’t affect the stability of the main codebase.
Version control systems also facilitate code reviews by providing a traceable history of code branches or commit changes. Developers can collaborate on ideas, provide feedback, and improve code before merging it into the main codebase. You can quickly revert to the previous version even when some bugs make it to the main codebase.
Version control, combined with continuous integration (CI) — a staged process that automatically ensures that new code versions can be released into production — tremendously increases your deployment capabilities.
Facebook's engineering team, for example, can push hundreds of diffs (versions of the code file) every few hours. The changes in the new system go through automated internal tests and land in the master branch, accessible to the entire team. Releases are rolled out in a tiered fashion over a few hours, so if any problems arise, engineers can stop the push.
Such a system allows Facebook to release new code frequently without risking user experience. Commits can be stopped and reverted. Code changes that previously required hotfixes can be committed to a master branch, updated, and added to the next release cycle. The framework also forces the engineering team to automate as many processes as possible and continuously improve the underlying infrastructure (push tools, traffic routing systems, etc).
7. CI/CD Enables Higher Team Velocity
As mentioned, continuous integration (CI) assumes automatic integration of code changes into the main branch as often and quickly as possible. To avoid conflicts, developers run automated tests against each proposed build before merging. Software engineering teams prevent integration issues before the release by doing continuous tests while maintaining a consistent codebase.
Continuous delivery (CD) is an extension of the CI pipeline that automatically deploys code changes to a staging and/or production environment. Apart from automating the testing process, you also automate the release, meaning you can release updates as early as possible.
Organizations using CI/CD tools excel in metrics like lead time for code change, deployment frequency, and time to restore service, compared to those who don’t.
Take it from the Netflix engineering team, which considers a stable CI/CD system a pillar for maintaining high product stability and team velocity. “When developers trust that their changes will not break production systems, they are able to focus more on the velocity of innovation, and not spend excessive time on manual validation of changes made,” says Phillipa Avery, Manager of the Java Platform Team at Netflix.
To ensure high developer confidence, Netflix relies on several validation steps in its CI/CD process, including library version dependency locking (to isolate failures), automated integration and functional tests, and canary testing. The team also uses microservice architecture patterns to reduce the dependencies between services further and shorten testing times.
A failure of a dependency version can be easily rolled back or locked to a previous version. Test failures can also be analyzed, fixed, and modified to move forward with the release, and so do canary failures. Netflix can effectively balance velocity vs. stability metrics by adding checkpoints but not putting hard breaks into its CI/CD. Each service team sets a threshold they feel comfortable with and such that aligns with the business requirements.
8. Always Write Descriptive Commit Messages
Always leave descriptive commit messages for your peers to improve the release process further. A good message should convey the intent and purpose of a code change and save tremendous effort on code reviews.
As the codebase grows, descriptive messages help locate specific topics within the version control history, exchange knowledge and minimize the risks of failed deployments. Descriptive commit messages also help generate better release notes and changelogs for other project stakeholders.
Many software engineering teams use the Conventional Commits specification to create standardized, explicit commit messages with a fixed structure:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
9. Keep Development, Staging, and Production Similar
Historically, development and product environments were quite disjointed because they were operated by different teams (developers and ops engineers), both relying on different tech stacks. DevOps, both as a cultural and technology shift, attempts to bring the two parties into closer alignment.
Parity between development and staging/production environments is important because it makes the release process easier. When there are significant differences in infrastructure configurations or in-memory cache requirements, both teams spend more time on issue troubleshooting, instead of building and committing new code. Developers get frustrated that a service that runs well on local resources doesn’t work in staging.
Container services like Docker and Puppet help create local environments that closely mirror production in seconds, making apps and their dependencies easily portable across environments. Containers can be quickly tested, deployed, stopped or scaled, speeding up the software development life cycle. Thanks to version control, you can also roll back any failed deployments.
Parity must also be maintained between sandbox (testing, QA, staging) and production environments. Implement the same infrastructure configurations — number of available nodes, database server configurations, load balancing etc. — to prevent failure. Tools like Azure Resource Manager and AWS CloudFormation allow you to create standardized templates for provisioning infrastructure to all environments.
10. Create Golden Paths to Start New Projects Faster
Golden Path is a template or a tutorial that engineering teams can use to complete common engineering tasks. The concept was first coined by the Spotify engineering team and later picked up by DevOps teams at Google, Netflix, and many others.
According to Spotify, Golden Path is “an "opinionated and supported" path to "build something" (for example, build a backend service, put up a website, create a data pipeline).” It offers a tutorial, a set of recommended tools for the task, and supporting documents.
Spotify further elaborates how Golden Paths, combined with CI/CD, helped skyrocket developer productivity. Instead of starting from scratch, a Spotify developer can use an existing website template from the company’s developer portal. The portal automatically generates a new Google Cloud Platform environment for the project with the correct configurations, a GitHub repo, and a CI/CD configuration. With most of the heavy lifting out of the way, developers can start committing new code almost instantly. Before this new website development took about 14 days, now it’s less than five minutes.
By automating infrastructure provisioning and tutorials, software engineers don’t need to start from scratch regarding trivial tasks. Instead, they can channel their productivity and creativity to more complex objectives.
Final Thoughts
Software engineering requires a continuous commitment to self-improvement. Although many fundamental software engineering best practices have been true for several decades, new approaches have emerged: DevOps and its more recent iterations like DevSecOps, GitOPs, AIOps, and site reliability engineering (SRE).
The best software teams are also constantly improving their toolkits, adding new tools for container orchestration platforms, infrastructure as code (IaC), test automation, and release management. Excellent in software engineering isn’t a destination — it’s a continuous journey.
Looking for an experienced software engineering team? Svitla Systems can help you ship stellar products with AgileSquads or a managed team extension model. In both cases, you benefit from our lean processes, strong coding standards, and an overarching commitment to high software quality and team velocity. Contact us to discuss these cooperation opportunities.