Last updated: Nov 29, 2024

Technical Debt, Quality, and Trade-offs: Striking the Right Balance

Written by Justin Ammerlaan and Guy Coleman · 12 minutes read

Technical Debt, Quality, and Trade-offs: Striking the Right Balance

Struggling to balance speed, quality, and technical debt in your software projects? Discover actionable strategies to manage trade-offs, prioritize critical decisions, and deliver reliable products faster. Dive into insights from Empower’s CTO, Justin Ammerlaan, and learn how to leverage technical debt without compromising on quality. Read now to transform your engineering processes!

Justin Ammerlaan and Guy Coleman

As engineers, we are constantly juggling the demands of delivering high-quality products quickly while also maintaining the integrity of our codebase. One of the most critical aspects of this balancing act involves managing technical debt, understanding what quality truly means, and making informed trade-offs. This post explores these concepts and offers insights into how teams can navigate these challenges effectively.

Our Co-Founder and CTO at Empower, Justin Ammerlaan has laid the foundation through years of experience building successful financial service products for how we can and should leverage “Technical Debt” and view and challenge concepts of “Quality” at Empower.

Understanding Technical Debt

Technical debt is a metaphor for the future cost incurred when opting for a quicker solution now, rather than a longer, potentially better approach. The alternative might be simpler, more flexible, adaptable, robust, or easier to maintain in the long run. Essentially, it’s the trade-off of immediate action for a known solution versus investing time in developing a potentially superior one. Like financial debt, it’s a form of borrowing now with the obligation to repay later.

“In finance, using debt as a tool is a way to leverage, or increase returns. In our case, in Engineering, it is a tool to create speed. It is an essential part of any financial strategy and also an essential part of the MVP process.” - Justin Ammerlaan

“Strategic use of technical debt can be a tool for creating speed and leveraging opportunities. However, like financial debt, it needs to be managed carefully. Accumulating too much without a plan for repayment can hinder progress and introduce significant risks to the project.” - Justin Ammerlaan

Just like with financial debt, technical debt needs to be repaid and should be repaid on a schedule otherwise the ‘interest’ starts to compound and the maintenance overhead becomes a massive burden. At Empower, we allocate 30% of engineering resources to non-product work, including technical debt.

Defining Quality

Quality is a foundational aspect of software development that ensures the product meets the needs and expectations of its users. However, misconceptions about what constitutes quality can lead teams astray. By clearly defining what “quality is” and what “quality is not,” we can align our efforts toward delivering value effectively. Justin provides some specific guidelines on how to think about what quality is and what it isn’t, let’s dive deeper into these concepts

Quality Is:

Correctness (Functionality)

Correctness ensures that the software behaves as intended, fulfilling all specified requirements without bugs or errors. It means that every feature works precisely as designed and anticipated.

Example: In a financial application, correctness ensures that all calculations (like interest computations or balance updates) are accurate, preventing financial discrepancies.

Accuracy (Adherence to Requirements)

Accuracy involves meeting the specifications and requirements. It ensures that the software solves the right problem and aligns with stakeholder expectations.

Example: If stakeholders need a report-generating tool that exports data in CSV format, accuracy means delivering exactly that feature—not an elaborate reporting suite with formats they don’t need.

Quality Is Not:

Optimization

While performance is important, premature optimization can lead to unnecessary complexity and wasted resources. Optimization should be based on actual performance needs rather than theoretical improvements.

Example: Spending days optimizing a function that runs in milliseconds, when users wouldn’t notice the difference, instead of working on delivering new features or spending excessive time optimizing a rarely used feature can divert resources from more impactful areas.

Generalization

Engineering solutions to handle a wide range of scenarios beyond current requirements can complicate the codebase and hinder maintainability. It’s better to address specific needs and adapt as they evolve.

  • Simplifying Design: Keep the design focused on current requirements to enhance maintainability.

Example: Creating a complex plugin architecture for an application that is only ever going to need the built-in functionality, or creating a universal data processing framework for all possible future use cases can be overkill if current needs are limited and well-defined.

Extensibility

Building systems with the capacity for extensive future expansion can introduce unnecessary complexity. Building hooks or frameworks for features that may never be implemented can introduce unnecessary complexity. Extensibility should be a consideration when there’s a clear, present need for future enhancements.

  • YAGNI Principle (You Ain’t Gonna Need It): Don’t build features based on speculative future needs.

Designing a module with extensive API endpoints for integrations that are not planned nor needed in the foreseeable future, or adding numerous configuration options for potential future features can make the system harder to understand and maintain.

Elegance

While elegant code can be beautiful, overemphasis on elegance can hinder progress. Pursuing sophisticated or “elegant” designs at the expense of practicality can lead to overcomplicated solutions. Practicality and simplicity often take precedence over theoretical elegance.

Example: Using a complex design pattern for a simple problem can make the code harder to read and maintain without providing significant benefits. A well-understood approach would suffice and expedite delivery.

Personal Coding Style Preferences

Imposing individual coding styles over team-established conventions can disrupt consistency and collaboration. Adhering to agreed-upon standards ensures that the codebase remains uniform and understandable by all team members.

Example: A developer insists on using a niche programming language feature that others on the team are unfamiliar with, making the code harder to maintain.

“Quality is something that should never be sacrificed when it comes to engineering at Empower. We are a financial tech company that moves millions of dollars every day and a mistake in specific areas of the code base can create significant issues. It is important we apply a lens of scrutiny with all we do – some items require intense care and review; others may be less so.” - Justin Ammerlaan

“Quality should not be confused with building perfectly designed systems that handle hundreds of use cases, when in reality what we really need to build is a hyper-specific solution so that we optimize time to market and bring learnings forward.” - Justin Ammerlaan

Understanding what quality is and what it is not helps guide our engineering teams into the “pit of success”, or pit of ‘productivity’ success. Our engineering teams can deliver reliable and valuable software by focusing on correctness and accuracy, simultaneously avoiding pitfalls like premature optimization and over-engineering. This approach ensures that resources are used wisely, and the software remains maintainable and user-focused.

Ultimately, quality in our engineering organization is about delivering solutions that meet our user’s needs efficiently and reliably without succumbing to unnecessary complexities or distractions. By adhering to these principles, our teams create products that function well and provide meaningful value to the business and our customers.

Trade-offs

Balancing quality, technical debt, and speed of delivery requires careful consideration and strategic decision-making. The key is to adjust these trade-offs relative to the requirements of the solution and the consequences of the decisions involved.

Balancing Technical Debt and Quality

Technical debt is a tool that can be leveraged to increase development speed. However, the amount of technical debt incurred should align with the specific needs and risks associated with the project. Justin emphasizes that it’s not a one-size-fits-all scenario; instead, it’s something that can be dialled up or down depending on various factors, like:

Balancing Trade-offs for Speed and Learning

When developing a product with uncertain customer demand, it may be advantageous to accept higher technical debt to deliver quickly and learn from real-world usage.

Example: If you’re experimenting with a new feature that might not resonate with users, it’s better to release a basic version rapidly. This approach allows for quick validation without over-investing in a potentially unviable solution.

Balancing Trade-offs for Critical Systems

Conversely, when working on foundational systems where errors can have significant consequences, minimizing technical debt is crucial.

Example: Developing an authentication system for a credit card application requires a high degree of reliability and security. Cutting corners here could lead to severe security breaches, so investing time to ensure robustness is essential.

Type 1 and Type 2 Decisions

Understanding the nature of the decisions being made is vital in determining how much technical debt is acceptable.

Technical Debt, Quality, and Trade-offs: Striking the Right Balance illustration 1

Type 1 Decisions (One-Way Doors)

These are significant decisions that are difficult or impossible to reverse. They require thorough analysis and deliberation because their impact is long-lasting.

Example: Choosing the core architecture for a system or selecting a database technology. Once implemented, changing these decisions involves substantial effort and risk.

Type 2 Decisions (Two-Way Doors)

These are decisions that are relatively easy to reverse or adjust. They allow for quicker action and experimentation since the consequences are limited.

Example: Tweaking the user interface design or adjusting a non-critical feature based on user feedback.

Applying the Trade-off Framework

Justin folds these concepts into a clear framework that our engineering teams apply to accurately balance trade-offs by accurately understanding the nature of the decisions going into any piece of work. When making trade-offs, consider the type of decision at hand:

For Type 1 Decisions

  • Minimize technical debt.
  • Invest time in ensuring high quality and robustness.
  • Perform comprehensive analysis and testing.

“The cost of reversing these decisions is high, so it’s prudent to get them right the first time.” - Justin Ammerlaan

For Type 2 Decisions

  • Accept higher technical debt if it speeds up delivery.
  • Focus on agility and learning from quick iterations.
  • Be prepared to adjust or discard these decisions based on feedback.

“Since these decisions are easily reversible, the cost of the technical debt is lower.” - Justin Ammerlaan

Tweaking Some Knobs and Pulling Some Levers

Justin points out some specific technical variables that can and should be adjusted based on the trade-offs accepted. These can be modulated to effectively manage these trade-offs and get to a great outcome for the business and its customers.

Importantly, adjusting these variables should not compromise the correctness or accuracy of the solution. The core functionality must remain reliable and meet the required specifications to deliver real value to customers.

Level of Abstraction Applied

Dial Down: Use concrete implementations to save time. Trade-Off: May reduce flexibility and increase coupling.

Level of Documentation

Dial Down*:* Provide minimal documentation to accelerate delivery. Trade-Off*:* Can impact future maintenance and onboarding.

Level of Decoupling

Dial Down*:* Accept tighter coupling for speed. Trade-Off*:* May make future changes more challenging.

Performance Tuning

Dial Down*:* Delay optimization efforts like caching or indexing. Trade-Off*:* Initial performance may be suboptimal but acceptable for validation purposes.

Practical Implications

Applying the principles of managing technical debt and making strategic trade-offs has significant practical implications. The approach varies depending on factors like the stage of the product and the criticality of the system being developed. Understanding when to prioritize speed over quality—or vice versa—is essential for delivering value effectively.

Early-Stage Products

In the early stages of product development, especially when exploring new ideas or entering uncertain markets, embracing higher technical debt can be a deliberate strategy. The primary goal at this phase is to validate ideas quickly and gather real-world feedback. Here’s how this approach may play out.

Scenario: A Startup is developing a new social media app with a unique sharing feature. Uncertain about how users will receive this feature, the development team decides to build a basic version quickly. They accept technical debt by not optimizing the code or building an extensive backend infrastructure. After releasing the MVP, they gather user feedback, which helps them decide whether to enhance the feature, pivot to a new idea, or discontinue it altogether.

Embrace Higher Technical Debt to Validate Ideas Quickly

Speed is of the essence when testing hypotheses about customer needs or market fit. Accepting technical debt allows teams to develop and release features rapidly.

Focus on core functionality with minimal embellishments. Use simple, direct code that meets immediate requirements, even if it’s not optimized for future scalability.

Focus on Delivering a Minimum Viable Product (MVP) to Gather Feedback

An MVP allows you to test the waters without overcommitting resources. It provides insights into what works and what doesn’t from the user’s perspective.

Prioritize features that offer the most significant user value. Collect data on user interactions to inform future development.

Be Prepared to Pivot or Iterate Based on Customer Responses

Flexibility is crucial. Early feedback might indicate a need to change direction or adjust features.

Maintain an adaptable codebase where changes can be made without excessive rework. Keep documentation minimal but sufficient to understand the code during iterations.

Mission-Critical Systems

When working on systems where failures can have severe consequences—such as in healthcare, finance, or aviation—the approach shifts significantly. Here, quality and stability take precedence over speed. The focus is on delivering reliable, secure, and well-tested software.

Scenario: A financial institution is developing a new online banking platform. Recognizing the importance of security and reliability, the development team prioritizes building a robust authentication system. They avoid shortcuts that could introduce vulnerabilities, even if it means a longer development timeline. Extensive security testing is conducted to ensure compliance with regulatory standards and to protect user data. Technical debt is minimized to maintain the system’s integrity over time.

Prioritize Quality and Stability Over Speed

In mission-critical systems, errors can lead to substantial financial loss, legal issues, or even endanger lives.

Invest time in thorough planning and design. Ensure that every component meets high standards of reliability before deployment.

Invest in Thorough Testing and Validation

Rigorous testing minimizes the risk of defects reaching production, which is crucial for maintaining system integrity.

Use comprehensive testing strategies, including unit tests, integration tests, system tests, and user acceptance tests. Employ code reviews and static analysis tools to catch potential issues early.

Limit Technical Debt to Avoid Future Risks and Costs

Technical debt in critical systems can lead to costly refactoring and increased maintenance burdens, compromising the system’s dependability

Adhere to best practices and coding standards. Document code extensively to aid future maintenance. Plan for scalability and future enhancements to reduce the need for significant overhauls.

Key Take-Aways

Strategically Manage Technical Debt Leverage technical debt deliberately to accelerate development, treating it as an investment that can yield significant returns if managed wisely. Maintain a repayment plan to mitigate risks, but also continuously evaluate whether the value derived from the debt outweighs its ongoing costs. If the cost of maintaining the debt begins to exceed its benefits, consider unloading it. This balanced approach ensures technical debt remains a tool for growth rather than a liability.

Quality Equals Correctness and Accuracy Focus on ensuring the software works as intended and meets requirements, avoiding misconceptions that detract from delivering real value. Balance Trade-offs Based on Decision Types Understand whether decisions are hard-to-reverse (Type 1) or easily reversible (Type 2) to appropriately balance technical debt, quality, and speed. Adjust Technical Variables Without Compromising Core Functionality Modify aspects like abstraction and documentation to manage trade-offs, but never compromise on the solution’s **correctness **and accuracy.


About Justin Ammerlaan

Co-founder and CTO of Empower Finance

Software expert with a wealth of technical and commercial skills. Extensive experience in finance software, including high transaction flow and modelling systems.

Justin Ammerlaan

Keep reading

More in Engineering