Philosophy of programming

Why do we emphasise crafting clear, readable and elegant code? Why do we rely on acronyms like DRY, SOLID, YAGNI, KISS to elevate engineering quality? Why do we produce documents such as Agile or Software Craftsmanship manifestos to steer our software development principles?

Philosophy of programming

(source: Reddit)

In this blog post, I will explore what constitutes a “good code” and argue that this may be a matter of perspective, the phase of the project and the company you’re working at. I’ll look at different approaches to aligning our technical standpoint with the business needs of the project.

There's no perfect code

Almost every change can have a comment in the code review process. Does it mean that every code has something to be improved? No, not necessarily, we just usually have different preferences as to how the code is structured and which way is better. You can name a variable in five different ways and it will convey very similar meaning, but your colleague may prefer a specific naming due to her personal inclinations. Even if you have the most comprehensive code guidelines, there will always be something to argue about. Writing code is a creative process, like in writing a book, an article, (or a blog post :)), there is no single way to write it. Therefore, it’s important that we understand other people’s styles, preferences and conventions as long as they are in line with general guidelines in your company.

Aligning software principles

Did you ever try to follow a few software principles at once? How did it go? Did you find a path to a perfect code? I would guess that you rather had a strange cognitive dissonance – something did not add up together. Looking at your code from different angles may result in conflicting approaches, and it's usually a matter of common sense to determine which one to use for a particular change. Let’s examine a few examples.

Scenario: Refactoring a legacy system

1. DRY (Don't Repeat Yourself) vs. YAGNI (You Aren't Gonna Need It):

  • DRY:  You decide to refactor repetitive code blocks by creating a shared library for error handling across multiple modules, aligning with the DRY principle to avoid redundancy. The library will be placed in a separate repository and published via a package manager.
  • YAGNI: Given the project's limited scope and deadline, you opt to make minimal, local improvements without investing time in a shared library, adhering to YAGNI's philosophy of not building features until they are necessary.

2. KISS (Keep It Simple, Stupid) vs. SOLID:

  • KISS: Faced with a legacy e-commerce system, you opt to simplify the existing payment processing code. Instead of introducing new classes, you refactor the code into a more straightforward, linear sequence of steps. This approach makes the code easier to read and understand, especially for new developers who may not be familiar with complex design patterns, adhering to the KISS principle of simplicity.

  • SOLID: Conversely, considering future adaptability, you choose to implement SOLID principles. You break down the payment processing module into several smaller, single-responsibility classes – one for validating payment details, another for processing transactions, and a third for handling receipts. While initially more complex, this approach sets a foundation for easier modifications and additions in the future, like integrating new payment methods or handling different types of transactions.

Scenario:  Designing a new cloud-based project architecture

1. DRY vs. KISS:

  • DRY: In setting up a cloud environment for a multi-service web application, you implement a centralised configuration management system. This system stores all environment variables, database configurations, and service endpoints in one place. By doing this, you ensure that any changes in the configuration are made once and reflected across all services, adhering to the DRY principle to avoid repetitive configuration setups across multiple services.
  • KISS: Alternatively, you decide to maintain separate configuration files for each service – one for the user authentication service, another for the data processing service, etc. This approach makes each service's configuration straightforward and isolated, reducing the complexity for developers who might only need to work on a single service. It aligns with the KISS principle by keeping things simple and avoiding the potential complexity of a centralised configuration system.

2. SOLID vs. YAGNI:

  • SOLID: Anticipating future growth and potential changes in requirements, you design the architecture with multiple layers of abstraction. You implement a service-oriented architecture (SOA) with clearly defined interfaces and services that can be easily replaced or updated. This approach, in line with SOLID principles, ensures scalability and flexibility but adds initial complexity and development time.
  • YAGNI: Considering the current needs and constraints, you opt for a more monolithic architecture. This architecture focuses on delivering the core functionalities required at the moment, with less emphasis on abstract layers or service decoupling. It aligns with the YAGNI principle by not investing in complexities that are not yet required, thus reducing the initial development time and complexity.

Do you already see where I am going with this? There is just no way to combine these every time. You will always bump into some conflicts along the way. These principles are a way of guiding us towards a better understanding of what we should aim for. The goal isn't to rigidly choose one principle over another but to find a harmonious balance that best serves the project's needs. The principles that we prioritise may change over time. It is best to consider an iterative approach, where you can change your choice later if a specific approach doesn’t serve your needs in the end.

Let’s consider a more complex in the scope of system design – LinkedIn. In the early 2010s, LinkedIn was one of the first companies to join the wave of mobile technology adoption [1]. Initially, they started with a hybrid mobile app development approach – using HTML5 and JavaScript wrapped in a native app shell. You may say that this approach is consistent with the KISS (Keep It Simple, Stupid) and YAGNI (You Aren't Gonna Need It) principles. It was simpler (KISS) and quick to implement, and at that time, it seemed sufficient (YAGNI).

However, the hybrid approach had its limitations, mainly concerning memory usage and user interactivity. Because the app was essentially a web page displayed in a native wrapper, it didn't perform as well as a native app, and the user experience was less fluid and intuitive than what competitors could offer with their native applications.

In 2012, LinkedIn chose to transition away from the hybrid architecture to a completely native application, specifically for iOS and Android. This move was a nod towards the principles of SOLID design, the utilisation of clean, maintainable, and scalable software architecture. Leaving behind the quick-and-easy hybrid solution, they settled on a more robust, even though complex, development approach that ultimately put them in a better position for long-term success [2]. Facebook made a similar move for iOS in 2012 [3].

(Source: Multidots, you may look at the full infographic here.)

Code reviews

Let’s consider another step in the software improvement process: code reviews. Have you ever engaged in an extended discussion about a small section of code that your colleague disliked? What impact did the proposed change have? Was it related to a significant bug discovered by your colleague? I would assume not.

The longest code review discussions are probably the least significant ones, where two different “religions” meet. You think you’re in the right, that someone completely misunderstands software development, and you’re angry that you have to explain it multiple times. This results in extended ping-pong-style code reviews that usually don’t significantly benefit the code. It’s important to realise that there isn’t a single way to implement a feature, and as long as the change does improve the existing code, minor differences can be let go.

Best code review practices probably deserve another blog post, but a few guidelines I find useful are [4, 5]: 

  • Generally, prioritise approving a PR if it enhances the overall code health, even if it's not perfect.
  • Skip easy errors. Issues like “wrong formatting, extra space, extra blank line” are effortless to find and only make you feel like you’ve done a comprehensive review. Focus on the important stuff and make a general comment about the style instead of commenting on each tiny issue. Consider introducing a common EditorConfig or automatic solution like Prettier, ClangFormat or Black.
  • Consider why the change was made. If you want to improve the proposed solution, think about how your proposal fits the original purpose.
  • Understand how the change impacts other parts of the system. Changes affecting multiple services can often remain unnoticed until deployment, making it vital to consider them during this phase.
  • Factor in backward compatibility. If something unexpected occurs, can you easily disable or revert it?

I think the perfect summary of the code review process comes from Google Engineering Practices:

“Developers must be able to make progress on their tasks. If you never submit an improvement to the codebase, then the codebase never improves. [...] On the other hand, it is the duty of the reviewer to make sure that each PR is of such a quality that the overall code health of their codebase is not decreasing as time goes on”. 

If we fail to balance these two conflicting principles, we will fail to improve the project codebase. Either by preventing improvements that never go through code review or by letting in changes that decrease the quality of the code.

Stage of the project

When looking at your code and the must-haves you want to include, even before creating a pull request, it’s important to consider the stage your project and company are at. Is it a greenfield project for a startup with a vaguely defined business logic and domain model?

Or is it a 30-year-old monolith for a big corporation with millions of lines of code? You may be somewhere in between, but it is important to notice that different projects will have very different definitions of “good code”.

Philosophy of programming

(source: Reddit)

Let’s examine these two examples in a little more detail. We will consider a few different “good code” categories.

Design

Startup:

  • Favours modern, scalable, and flexible designs that accommodate quick shifts and changes in business requirements.
  • Prefers simplicity and minimalism to allow easy understanding and iteration.
  • Design patterns and architectures that support rapid development and deployment, like microservices or serverless, are commonly used.
  • Example: Let’s create a new serverless function for this functionality, it’s a completely different part of domain logic. It doesn’t depend on any existing logic, so it will be easy to implement.

Corporation:

  • Compatibility with the legacy design is vital, so new code must coexist with existing structures.
  • Prefers incremental refactoring of the design to improve modularity and maintainability without breaking the current system.
  • Any new design should integrate well with the old system and respect its constraints.
  • Example: We would like to add a new functionality to this service, but it would be much easier to do it in a new framework. The feature will reuse parts of existing logic. However, the current service has a lot of dependencies that will take months to migrate. How do we approach this?

Delivery deadlines

Startup:

  • Usually, rapid feature deployment will be prioritised to gain a competitive market edge or validate ideas with users.
  • Good code meets deadlines through iterative and incremental improvement while getting usable solutions out fast.
  • Deadlines will usually be dynamic, enforced internally, rather than due to external commitments.
  • Example: Can we have this feature delivered by Monday, so we can show it to the investors? We are not sure whether they’re going to like it, so we may remove it later.

Corporation:

  • Deadlines are still important, but there's a balance between the speed of delivery and the risk of disrupting stable, mission-critical systems.
  • Good code in this scenario includes thorough planning and testing to minimise risks.
  • Deadlines can be rigid and enforced externally due to external commitments or law obligations.
  • Example: We have to deliver this change by the 2nd of February, when the new law comes into effect. We still need to test it thoroughly, a bug in this part of the system would be catastrophic.  

Development speed

Startup:

  • Development speed is often a high priority to quickly respond to market changes and user feedback.
  • Coding standards and practices aim to minimise technical debt while still allowing for a fast pace of development, favouring conventions over configuration, and using high-productivity frameworks and tools.
  • Example: “CEO”: Hey Przemek! I have this new idea… How quickly do you think we can have it in production?

Corporation:

  • Speed is balanced with the need for stability and reliability. Changes may be slower due to the layers of testing and review required.
  • Good code practices may focus on automating processes and improving workflows to increase the speed of development where possible without compromising on quality or reliability.
  • Example: If the feature successfully goes through testing, we will deliver this feature next month, but it can be delayed until the end of the year.

Performance

Startup:

  • Good code is expected to be performant but might trade-off some performance for faster development if necessary, especially in the early stages.
  • Focus on performance might be balanced with the need for quick deployment and iteration.
  • Example: Using ORM for database access will be sufficient for us. We don’t need any extra indexes in the database for now. Our database contains only thousands, maybe a few millions of records.

Corporation:

  • Performance is often critical in well-established codebases where efficiency impacts cost and customer experience.
  • There’s an expectation that new changes do not degrade existing performance, and where possible, they improve it.
  • Example: For complex SQL queries, we need to consider writing a stored procedure or “hacking” ORM to generate a query in a certain way. We should also examine whether we need to create or update an index after we update the query. Our database contains millions or even billions of records.

Tests

Startup:

  • Test coverage might start out lower but is expected to grow as the project matures.
  • Emphasis on automated testing to support a rapid development cycle.
  • Tests may be temporarily skipped for features that are not crucial to the app's capabilities.
  • Example: Do we already need acceptance tests for this feature? We are not sure whether it’s going to exist in a few months. We may also completely rearrange it, making existing tests unusable.

Corporation:

  • Good code comes with extensive tests, often due to rigorous requirements for reliability and stability.
  • Test suites may be large and complex, updating them can be just as crucial as the code itself.
  • There’s a focus on maintaining or increasing test coverage, even for legacy code, to prevent regressions.
  • Example: Please spend an extra day writing acceptance tests for this feature. We must be sure it’s working properly.

I highlighted categories that demonstrate the differences, but both projects could share common practices rooted in overarching software engineering principles. These could be security concerns, prioritising code readability, adhering to robust coding standards, or emphasis on the extensibility of the code.

The company and the project you’re in do matter. You can’t use the same approach in vastly different development dynamics. Understanding your project needs at a particular stage can be challenging, but it will make you much more flexible and valuable to your company. Even for two companies at the same stages, with similar projects, development practices can be markedly different. It’s important to understand whether you fit into the company culture and to sometimes consider another job without blaming yourself for a perceived failure.

Summary

Soo, we’re finally through it, 2600 words, I’m glad that you’re still reading (you didn’t just skim through to the summary, did you?). Writing “good code” will always be challenging, because, in the words of Google Standard of Code Review: “there is no such thing as "perfect" code – there is only better code”. Even the code generated by LLMs won’t be perfect and will require engineers' attention – I’m sure you wouldn’t just straight-paste LLMs’ code into your payment processing module. However, shipping your code to production is as important as making it technically excellent. 

Aligning different software principles together may give you a headache sometimes, cause arguments with your colleagues or even make you reconsider your career in software development :D. It’s useful to have them in the back of your head because all in all, they do help a lot if you sift through them. Even the design of your product may be inspected from the standpoint of the software principles.

Code reviews are aimed at improving code quality. They’re an essential part of the process for the vast majority of modern software companies. It’s important to remember that code is only code, it doesn’t present the entirety of a person. Keeping empathy in mind will help you make code reviews more productive and efficient. A code left in the phase of PR is always a wasted effort.

Your company certainly has its own philosophy of programming. Whether it’s aligned with your view of “good code” may depend on what you value as a software engineer. Being more flexible with your views will allow you to thrive in a variety of software teams. Understanding the dynamics and culture of your company will lead you to deliver software aligned with your company’s goals.

Did you think about the philosophy of programming at your company? Perhaps you can see some of the previous examples in your everyday work? Or maybe, if you’re early in your career, you think you know the right way, because there must be a right way, right?

Feel free to reach out to me on LinkedIn or drop me an email. I am curious about your opinion!

References

  1. LinkedIn Gives Users An Android App. (2010, December 16). TechCrunch. Retrieved from: https://techcrunch.com/2010/12/16/linkedin-android/
  2. Why LinkedIn dumped HTML5 & went native for its mobile apps. (2013, April 17). VentureBeat. Retrieved from: https://venturebeat.com/mobile/linkedin-mobile-web-breakup/
  3. Under the hood: Rebuilding Facebook for iOS - Engineering at Meta.  (2012, August 23). Engineering at Meta. Retrieved from: https://engineering.fb.com/2012/08/23/ios/under-the-hood-rebuilding-facebook-for-ios
  4. The Standard of Code Review. Google. Retrieved from: https://google.github.io/eng-practices/review/reviewer/standard.html
  5. Expectations, Outcomes, and Challenges of Modern Code Review. (2013, September 16). Alberto Bacchelli. Retrieved from: https://sback.it/publications/icse2013.pdf
Share the happiness :)