10 Software Engineering Meditations

This is a curated and opinionated list of thoughts, behaviors, and conclusions about the software development process as a whole. Your mileage may vary, but this list was meticulously crafted based on systematic observation, testing, successes, failures, and retries across different teams, companies, and countries.

1. Having any standard is much better than having no standard.

In software teams and companies, entire months can be wasted in heated discussions about whether developers should use tabs or spaces, or whether to use branches or trunk in Git. When these discussions become especially heated or prolonged, teams often decide not to enforce any standard, as no unanimous decision is reached. While this might seem like common sense, in my opinion, it’s the wrong decision. Not only have you wasted a lot of precious time for multiple people, but you’ve also ended up with a worse outcome than when you started.

After the heated debate, most people will have likely chosen a side, and as a result, everyone will start using their own preferred standard. Before this, there may have been a “soft” or “implicit” standard in place. Now, however, competing standards will make life harder for everyone.

These inconsistencies will lower code quality, make maintenance and onboarding more difficult, and create unnecessary friction between team members or teams.

If you cannot agree on a standard, just pick the easiest one and re-evaluate the results later. Depending on the impact of the decision, you can reassess next month, next quarter, or next semester. The outcome could be that the new standard works and everyone is happy, or that it didn’t work, in which case you can either make adjustments or try the other option.

In the meantime, you’ll have a more consistent setup, which will improve readability, maintainability, and the onboarding of new people, even if the chosen standard isn’t the best one available.

2. When in doubt, play by the book first and adapt later.

This principle applies to both technical and managerial decisions. If you can’t agree on whether Scrum should be implemented with 2-week or 4-week sprints, start by trying the recommended approach (2 weeks) and reevaluate later to see if it worked. It’s better to discuss the issue later with real-world data rather than speculate pointlessly.

The same applies to technical decisions. However, extra caution is needed because some technical choices can have long-lasting impacts and consequences that are difficult or costly to change. In some cases, it may even be technically unfeasible to reverse the decision.

For instance, let’s say you’re deciding which Java framework the company will use in the future, choosing between Spring or Micronaut. This decision will affect almost every other technical decision moving forward, so it makes sense to take the time to thoroughly evaluate the options.

That said, if you still can’t decide on a clear winner after thorough evaluation, you should settle on the recommended tool for your use case. Be sure to consider non-technical aspects such as:

  • Popularity and community support
  • Open-source vs. closed-source considerations
  • Free vs. paid options
  • Ease of use
  • Vendor lock-in implications

This approach will make your life easier in the future, even if you end up changing your decision down the road.

3. Bad testing is worse than no testing.

Testing and good testing practices are key to building high-quality software quickly. That said, writing tests just for the sake of it can make things much worse in the long run. Incomplete or poorly written tests can give developers a false sense of security, which is dangerous because you could still be deploying incorrect or incomplete code to production without anyone noticing until it’s too late.

The Dangers of Inadequate Testing

Let’s say your project involves loan calculations. You assume the core loan logic is already tested because you have many tests in place, so you feel comfortable making changes. However, out of the five types of loans that exist, you’ve only properly tested three. Moreover, in pre-production, only the “happy path” for the most popular loan type is manually tested.

In this scenario, any developer could introduce a breaking change to the untested loan types, and the only safeguard might be code reviews (assuming they exist). If the error makes it into production, it could go unnoticed for long periods. Depending on the business, this could result in significant financial losses due to incorrect calculations, or even litigation from clients or regulators.

Quality Over Quantity

Setting arbitrary test coverage targets (e.g., 100%, 80%) can encourage this behavior, as developers focus on reaching the coverage target instead of writing quality tests. At the most basic level, never write a test case unless you are completely sure what it’s actually testing.

Ensure your tests don’t only cover the “happy path” or common errors. Always aim to cover all possible scenarios. When fixing a bug, write a test that reproduces the bug, then implement the fix. The fix should make the new test pass. Never commit a test without ensuring it fails when it should; a test that never fails is one of the worst culprits and everyone has wrote at least one of those.

In the end, you should treat tests as part of your codebase. Poor tests will lower code quality, make the system harder to maintain, and complicate troubleshooting. Even duplicate or useless tests still require maintenance and review, creating unnecessary overhead for your projects.

4. Involve stakeholders early and often.

One costly mistake (in terms of time and effort) made by many software teams is not involving stakeholders actively in the development process. Depending on your company, the stakeholders will differ, and how you involve them may vary, but this step should not be overlooked.

Stakeholders, let’s say, for example, clients, cannot be involved only when a new feature is already in production or pre-production. That is far too late to ask for feedback. By that point, the work is already done.

The best time to identify issues or areas that need further clarification is when the tickets for the requirements are being written, planned, or refined. The second-best time would be just as you are assigned a ticket, and before you start working on it.

5. Trust the process.

In line with points 1 and 2, defining a concrete way of working is crucial for achieving measurable success. However, this doesn’t mean writing an entire book or 25 pages on how you’ll work. A concrete way of working can be as simple as “There is no plan,” or having 2 or 3 basic rules. The main thing is to define an explicit contract within the team on how things will be done.

Based on that, you can later reassess the results and realign. The key is to commit to a plan and execute it for a specific period of time so you can effectively measure whether it works or not. This period can range from a week to three months.

Commitment and Consistency

The crucial point is not to change the plan as soon as it shows signs of not working. Equally important is ensuring that everyone on the team is committed to the plan. If halfway through, people start doing things their own way, it becomes impossible to assess whether the approach was successful or not.

Then, as in the previous points, you can encounter a variety of scenarios:

  1. “Okay, after this two-week sprint, we tried working with no rules, and it was chaotic. Maybe we can introduce these five principles to work more efficiently.”
  2. “Having no rules was perfect! Everyone was able to work at their own pace, and we delivered everything on time.”
  3. “Having 25 rules was too much. Everyone felt pressured and micromanaged. Barely any progress was made, and team members started blaming each other. Maybe we should try reducing the number of rules and steps in the process.”

No matter the outcome, the important thing is that you now have a working framework through which you can iterate and improve team dynamics, eventually finding the best way of working for your own team. No two teams will be exactly alike: experiment, measure, and reevaluate.

6. Actively ask for help.

It’s no revelation that communication is key to a team’s success. However, people often feel uncomfortable asking questions in public, even within their own team. Developers should be encouraged to actively seek feedback or ask questions as soon as they encounter issues. This prevents mistakes from occurring, stops existing errors from spreading, and fosters team cooperation.

Creating Psychological Safety

This isn’t solely the personal responsibility of each team member. The team and company culture must explicitly and actively work to create a safe environment for questions, mistakes, and feedback. If people are regularly mocked or ridiculed for asking basic questions, they will likely become passive and withdrawn. When people don’t feel comfortable speaking up, it creates one of the worst possible scenarios for any company.

Imagine a developer finds a serious bug one day before going to production after two weeks of being pressured to deliver. Will they speak up immediately and prevent further damage, even if it means delaying deployment by an extra week?

Well, this will depend largely on the team culture. If the developer believes they will be harassed by the team for causing a delay, they might stay quiet and try to cover up the bug to avoid being blamed when someone else discovers it.

This scenario is not uncommon, and it can be catastrophic. Always encourage developers to speak up as soon as they notice something off, and praise them for doing so. Even if the question or double-check turns out to be “unnecessary,” it can spark further discussions, improve team knowledge, lead to refactoring, or enhance documentation for the future.

Asking questions now and finding out everything is “all right” is far cheaper than staying silent and discovering months later that the thing that seemed “weird” was a bug that cost the company or customers thousands of dollars.

7. Always keep customer value in mind.

As engineers, we often get blindfolded to the fact that technology is just a tool for achieving a concrete business goal. Technology is not an objective in itself, even in a tech company. We tend to chase the latest trends, solve the hardest puzzles, try to eliminate all technical debt, or work with an exciting tech stack simply because it will look great on our resume.

Aligning Technical and Business Goals

The reality is, most of us are not writing software as a hobby. We work for companies to be compensated, and these companies hire us to help achieve their business goals. This doesn’t mean we are robots that churn out code or that our relationship with the company is purely transactional, but we should always keep these concepts in mind, especially when making key technological decisions such as choosing the architecture of our systems or planning how to address technical debt.

Sometimes, using cutting-edge technologies like serverless functions (e.g., lambdas) in the cloud may be the best solution, but more often than not, a simple monolith running on a traditional VPS instance might be more appropriate. Sometimes, DynamoDB or Redis is the answer, but other times, MySQL will more than suffice, even if it’s not the most glamorous solution.

Make sure to regularly do reality checks for your decisions and never stop asking “why.” Is a message queue really the best solution here, or do I just want the chance to use Kafka in production? It may sound silly, but many of these decisions are made unconsciously. So be sure to ask these questions each time. You’ll be surprised how often you catch yourself making decisions for the wrong reasons.

This principle extends beyond technical decisions. When deciding which tickets to work on, it’s always a good idea to consider how much customer value they provide and the overall impact they will have (even though the Product Owner should primarily be responsible for this).

8. Follow the Boy Scout rule.

The concept is not new at all, as it was introduced to programming decades ago by Robert C. Martin. The rule states: “Leave things better than you found them.”

Managing Technical Debt Effectively

In programming, technical debt is unavoidable. Any attempt to prevent it completely is bound to fail. The key is having a plan to manage it effectively.

Tech debt, much like traditional debt, is not inherently bad. It’s a tool, and as such, it can be leveraged by mature teams to drastically improve development speed without compromising code quality in the long term.

To be successful at managing tech debt, you need to plan carefully which parts of the development process will allow debt to grow and when it will be addressed. One small behavior that can help with this management is precisely the Boy Scout rule.

Practical Application

The basic idea is to leave any piece of code you touch in a better state than it was. More specifically:

  • If you find a method that isn’t clear enough, refactor or rename it
  • If comments are missing, add them
  • If tests are missing, write them
  • If something is wrong, fix it, or at least bring it to the team’s attention

You don’t necessarily need to fix everything on the spot, as some issues may take too much time or be out of scope for the task you’re working on. However, this can trigger follow-up refactoring sessions, or the issues can be added to a refactoring backlog.

The rule of thumb is to use common sense: if something can be fixed while implementing the ticket you’re working on, just do it.

One important note: for this rule to be truly effective in the long run, the codebase must have a solid foundation of tests. If most of the code isn’t tested, many of these “small” refactorings could cause more issues than they solve.

9. Know Your Surroundings

As mentioned in point 7, we engineers tend to focus heavily on technical details. While this is beneficial for abstraction and solving complex technical challenges, becoming too isolated from the rest of the company can lead to other issues, such as spending months optimizing features no one uses or deploying services that are a nightmare to maintain in operations.

Develop End-to-End Understanding

This is why it’s important to be curious about how everything works end-to-end. Depending on the company and its size, your role as a software engineer might range from being very close to the client and operations, to being completely detached, where all you do is commit code to a Git repository, and that’s it.

Regardless of where you fall on that spectrum, it’s always valuable to have at least a rough understanding of how everything works at each level (even if you’re not the one handling it day-to-day). Consider questions like:

  • Do you know how your features run in production?
  • How do logging and notifications work?
  • Could you track down an issue just by looking at the logs?
  • Do you know how customers are actually using your features?
  • Do they complain about anything?
  • Does your feature consume too many resources?
  • Do other teams use your feature internally?
  • Are the current APIs effective?
  • Does anyone actually need them?

The list of questions you could ask is endless. Developing this broader understanding of the big picture will serve as a valuable feedback loop for decisions like those discussed in points 7 and 4, helping you build higher-quality software that better aligns with overall company objectives.

10. Avoid knowledge silos

Knowledge sharing and propagation are among the biggest challenges in large teams of any kind. As a result, the de facto tendency of big groups is to create knowledge silos. At one point, this was even institutionalized by management, as the standard way of organizing departments involved specialized teams (e.g., development, operations, etc.) with even more specialized members (e.g., backend, frontend, DBA, QA, etc.).

The DevOps Evolution

This trend began to reverse in the early 2000s but gained significant momentum after 2010 with the introduction of the DevOps concept. One of DevOps’ primary goals was to eliminate these knowledge silos and make development and operations workflows more agile and streamlined. This is also why DevOps pairs well with frameworks like Scrum, XP, and other agile methodologies.

There are several ways to implement DevOps:

  • Replace specialized teams with cross-functional development teams that can independently build, deploy, and handle operations end-to-end
  • Maintain a core IT operations team, now composed of Site Reliability Engineers (SREs), but without them being responsible for deploying or managing specific services
  • Ensure every service has an assigned owner who is responsible for it

The core operations team, or SREs, focuses on defining and implementing the tools and infrastructure that enable cross-functional teams to fully own each service. This includes, but is not limited to:

  • CI/CD pipelines
  • Cloud infrastructure management
  • Monitoring and alerting
  • Supporting development teams in troubleshooting issues

This disrupts traditional role specialization. In this paradigm, titles like “backend” or “frontend” engineer have much less rigid meanings, and everyone should be involved in some aspect of the entire end-to-end process, from ticket creation to production deployment.

It’s important to note that the key takeaway here is that DevOps is not just a change in workflow or roles; it represents a fundamentally different philosophy regarding how development and operations should be handled. You can draw parallels with the transition from the waterfall to agile methodologies in the development process.

Conclusion

The core idea threading through these principles is that effective software development requires conscious decision-making about processes, not just technology. By establishing clear standards, involving stakeholders throughout the development cycle, maintaining a holistic view of the system, and fostering open communication, teams can significantly improve both productivity and software quality.

Implementing these principles may require adjustments based on your specific environment, but the underlying concepts of standardization, pragmatism, quality focus, and knowledge sharing create a solid foundation for any software development team seeking to evolve and improve.

Leave a Comment

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top