The Difference Between Code as an Asset and Code as a Liability

The Difference Between Code as an Asset and Code as a Liability

When an expert throws clay pots, the quality of the clay is a critical factor in determining the outcome. The potter wants plasticity, which means the clay is mouldable and malleable. In the modern era, most companies are digital or transforming to be so. Their medium is the source code of their software systems. Just as the potter wants adaptable clay, the organisation wants adaptable and evolvable software systems. The organisation wants plasticity in this source code so it can mould and adapt its systems to enable business agility. This article covers both what not to do and what to do to achieve high-quality, adaptable code.

Is your team building a strategic asset or a technical debt liability? The difference almost always comes down to one critical failure: focusing on the trees (immediate functionality) and missing the forest (the overall architectural vision). When someone “just hacks it in,” there is no vision, no high-level goals, no forest view. They dive straight into the details—straight to the trees. Without that broader perspective, the team suffers the following consequences:

  • Roles and responsibilities are never defined.
  • Design principles are ignored.
  • Patterns of reuse are never discovered.

Instead, there’s a laser focus on the narrow area of interest. Implementation decisions are made only to solve the immediate problem in that tiny corner. The results are predictable:

  • Violation of single responsibility.
  • Rampant duplication.
  • The same concept re-implemented with slight variations.
  • One piece of code implementing all sorts of things.

With this shared understanding becomes impossible—only the original author (maybe) understands it.

No Vision = No Boundaries

Without high-level goals, the solution has no clear architectural borders. This lack of definition leads to components that are tightly coupled and conceptually confusing:

  • Unclear Ownership: Components bleed into each other. "Where does this responsibility actually end?" becomes unanswerable.
  • Accidental Coupling: Everything connects to everything because no one enforced separation of concerns, making the system difficult to modify.
  • Context Collapse: Business logic, data access, and user interface concerns are mashed together in single files or modules, violating layering and making testing impossible.

Trees Without Forest = Tactical Myopia

Narrow focus produces local decisions that are globally incompatible. When developers only focus on the immediate ticket, they create code that works today but poisons the future:

  • Pattern Blindness: The same problem is solved in different, inconsistent ways across the codebase (leading to the later symptom of rampant duplication).
  • Premature Optimisation: Developers optimise the wrong things because they can’t see which parts of the system actually matter to performance or scale.
  • Local Maxima: The code is "perfect" for today’s ticket, but the next feature will require a painful rewrite to get past the short-sighted design choice.

No Principles = Architectural Drift

When fundamental design principles aren’t enforced, system entropy wins. This causes the codebase to drift away from any sensible design, making it fragile:

  • God Classes Emerge: One class, table, or function does multiple things (e.g. handling authentication, validation, business rules, and persistence). The single responsibility principle is not applied.
  • Shotgun Surgery: Changing one behaviour requires editing many scattered files, a sure sign of poor cohesion and excessive coupling.
  • Fragile Code: One fixes one bug, only to introduce two more because of hidden, undeclared dependencies that the initial "hack" created.

Code-Level Symptoms (Code Smells)

These additional signs are often the direct results of the problems listed above and signal a significant architecture problem:

  • Poor Naming: Member names are inaccurate, overly general (data, temp, result, helper), or simple synonyms of each other, making intent opaque.
  • Misguided Comments: Comments explain what the code does (because the code itself is unclear) instead of why this specific design decision was made.
  • Rampant Duplication: The same concepts are implemented in multiple places, or the same logic appears with slight variations or repeats throughout the code, violating the DRY (Don't Repeat Yourself) principle.

This code is not an asset; it is a liability that drains budgets and slows feature velocity. Hopefully, code only gets this fragile at its end-of-life—10 to 15 years in. If a new solution is hacked together, you start at the end-of-life state on day one.

The cure for hacking is deliberate architectural vision and governance. Why is this important? Technical debt is a compounding relationship, much like a compound interest account. New interest is incurred on existing interest. similarly new technical debt is incurred on existing technical debt. No company can sustain the exponential costs associated with this.

This is why software architecture and design are essential. It is not an abstract activity performed by purists; it is the fundamental way we ensure software systems can be changed cheaply and reliably, now and in the future. It is a well-accepted fact that:

The initial project cost that "births" a software system is usually insignificant compared to the cost of changing that software over its lifetime.

We design to minimise those future, long-term costs.

The solution

So, how do we avoid this trap? The cure for hacking isn't more process, but deliberate architectural vision and governance.

Scrum and Agile software development aren't just about meeting names and formats—they also prescribe engineering practices that shape maintainable code. Maintainable code is code that can be changed cheaply and reliably, now and in the future. These changes include bug fixes, enhancing existing features, or adding new features. This article proceeds by describing the Agile design track. Aligning with this track will provide far more agility than aligning only with the Scrum track.

Article content

The Design Track is not a one-time activity; it's followed iteratively within and across every sprint. While some high-level architectural work is done during sprint planning, the bulk of the detailed design, application of design principles, and refinement happens continuously throughout the rest of the sprint. This constant, iterative focus is what keeps the code plastic.

Software needs engineering principles just like bridges do. Everyone understands that building a bridge requires proven engineering principles. No one starts pouring concrete and hopes for the best. We all accept that if those principles are ignored, the structure becomes unstable — and eventually collapses. Because it’s physical, we can spot the design flaws and can often see the cracks forming. In software, these cracks are often invisible until it's too late

Software is no different. It too must follow rigorous principles to remain stable. The difference? Software is invisible, an abstract logical structure. You can’t spot a missing beam or a untightened bolt just by looking. So what does “unstable” even mean in software? Stability is simply the ability to change the software quickly, safely, and without fear.

Software that is hard to change is unstable. When change is no longer possible, the codebase has effectively collapsed, even if it still runs. This is the moment the "clay" loses its plasticity. Here are the most important design principles that keep software strong, scalable, and maintainable.

The 5 SOLID Principles – Your Code’s Best Friends

  • S – Single Responsibility: Every piece of your program should have one job, and only one. Like a chef who only cooks (and doesn’t also wash dishes or seat guests), a class or function should do just one thing well.
  • O – Open for Extension, Closed for Modification: One should be able to add new features without touching old code. Think of LEGO: snap on new pieces without breaking what’s already built.
  • L – Liskov Substitution: If ones code works with a “parent” object, it must still work perfectly when one swap in a “child” object. Example: if something expects a Bird and calls fly(), a Penguin subclass shouldn’t crash everything.
  • I – Interface Segregation: Don’t force classes to implement methods they don’t need. Instead of one giant remote with 100 buttons, give users remotes with only the buttons they actually use.
  • D – Dependency Inversion: High-level parts of ones app shouldn’t depend on low-level details. Both should depend on abstractions (like contracts). This makes swapping components as easy as changing a car engine without rebuilding the whole vehicle.

Three More Golden Rules

  • DRY – Don’t Repeat Yourself: (also known as Once And Only Once when only considering the source code). Every piece of knowledge (e.g., a business rule, an algorithm, test case etc.) should exist in exactly one place. Copy-pasting code today guarantees one will fix the same bug in multiple places tomorrow.
  • High Cohesion: Keep related code together. Everything about “shopping carts” should live in the same module — not scattered across modules and files.
  • Low Coupling: Keep things independent. A change in one part of the system shouldn’t break unrelated parts. If remodelling the kitchen somehow kills the living-room TV, the design has a serious problem.

Automated testing, the enabling practice

Automated testing is the #1 enabler of agile development. It's the crucial safety net that lets teams refactor (restructure code without changing behaviour), extend, and improve the system with confidence—knowing immediately if a change accidentally broke existing functionality.

Article content

Ubiquitous Language is the practice of getting everyone—clients, domain experts, designers, and developers—to use the exact same vocabulary when discussing the real-world business domain. It starts by accurately understanding the domain and iteratively adding detail until a clear model emerges that drives the implementation of apps, services, and databases.

This step is foundational as without conceptual clarity, agility is impossible. The concepts and ideas in both the domain and the solution will not be clearly understood or expressed, leading directly back to the architectural chaos discussed earlier.

Fortunately, this is also the step where the business analyst's, UI designer's, and solution architect's practices overlap. Domain modelling is the one thing they all do.

Business analyst

This is where the business analysts will perform Concept and Data modelling. Concept modelling organises the business vocabulary, focusing on the core noun concepts of the domain. High value descriptions of the concepts, which are free from data or implementation bias are created.

Data modelling describes the entities, classes or data objects relevant to a domain. It describes their attributes, and the relationships among them. The model provides a common set of semantics for analysis and implementation.

UI designer

Designers focus on Information Architecture, the discipline of organising information for the user interface. To create an effective UI, they must first deeply analyse the underlying business domain: the entities, their attributes, relationships, and the overall quantum of data.

This analysis, which benefits from practices like Concept, Data, and Domain Modelling, ensures that the resulting user interface is appropriately designed. Crucially, the designer's work ensures the final user interface visually and functionally maps directly to the domain's agreed-upon ubiquitous language.

Solution architect

Object-Oriented Analysis and Design (OOA/D) is what sets object-oriented programming apart from other approaches. Before OOA/D, analysis and design were two unique activities. OOA/D changed this, requiring one to start by analysing the problem and continuously add more detail until the design for the system's components emerge. Domain-Driven Design (DDD) is similar, just a bit more opinionated when it comes to classifying the classes and defining their purpose.

Everyone

This step is foundational, so it's not surprising that it's practiced by business analysts, designers, and architects. Without it, agility of the solution is simply not possible. Without it, the concepts and ideas in both the domain and the solution will not be clearly understood and expressed separately.

Agile says it best: Ubiquitous language strives to use the vocabulary of a given business domain, not only in discussions about the requirements for a software product but in discussions of design as well and all the way into “the product’s source code itself”.

The goal is simple: to refine ideas and express concepts clearly, consistently, and separately so that the language of the business becomes the language of the code. This direct connection is the ultimate source of architectural plasticity.

Article content

The ubiquitous language/domain model produced drives the implementation of the Model layer in all components making up the system. It shapes database schemas and core business logic. As solution details are added (like the user interface, or View), the Model must be connected to it. This connection is built iteratively and incrementally by adding details to the Model, View and the connecting layer, which is given different names like Controller, Presenter, ViewModel, etc.

The critical principle here is Model independence. The Model (business logic and persistence) must be entirely independent of the View and Controller that use it. It should be possible to put a CLI, GUI, batch file, or any other interface on top of the Model. It should also be possible to embed the Model in multiple system components, improving performance by turning remote calls into local, direct invocations.

The main focus should be to keep the process and design as simple as possible. The following principles should be adhered to:

  • Design is an ongoing activity, which includes refactoring. Design is not a phase; it's continuous maintenance.
  • Apply design principles: Focus on SOLID, DRY, low coupling, and high cohesion at all times.
  • Avoid speculative design: One should not design for the future based on the assumption that the functionality will eventually be needed—You Aren't Gonna Need It (YAGNI).
  • Evaluate quality by simplicity: Design quality is primarily evaluated based on the rules of simplicity (e.g., does it maximise clarity and minimise elements?).
  • Justify Complexity: All design elements such as design patterns, frameworks, and plugin-based architectures are seen as having costs as well as benefits. The costs must be justified by clear, current needs.
  • Defer decisions: Design decisions should be deferred until the “last responsible moment”. This maximises the information collected on the benefits of the chosen option before incurring its costs.

Agile promotes Emergent Design, the idea that good global design results from consistently paying attention to the local qualities of code structure.

While this sounds like hacking—as both involve focusing locally on the code being changed—the critical difference is discipline and adherence to the vision:

  • Hacking: Prioritises the path of least effort (e.g. adding functionality to an existing class, copy and pasting code) and ignores design principles. It creates random, poor local choices that result in a brittle global system (Architectural Drift).
  • Emergent Design: Requires that global design principles are adhered to during every local change. Unlike hacking, emergent design always considers the integrity of the entire codebase when implementing a change.

By enforcing this discipline locally, the global architecture emerges as stable, maintainable, and robust—the exact strategic asset that hacking turns into a liability.

A common misconception about emergent design is that it reduces the need for design skill. In reality, the opposite is true. Significant design skill is required to ensure the correct design emerges. Furthermore, design is a continuous activity performed by all developers.

The same pattern repeats daily: Refactor the code first so that once the increment is complete, the code still adheres to SOLID, DRY, etc. This refactor first approach is the daily practice that separates emergent design from hacking. It is the application of significant design skill that results in the minimisation of later refactoring. Design is a team activity, and everyone should be contributing. If this is not the case, then the team does not possess the significant design skill required.

In corporations where software is not considered the main product, you often hear the instruction: "Implement the change, but do not touch the existing code." While software may not be the primary product, it is the main business enabler—a fact confirmed by the importance of digital transformation. This instruction to not change the existing code is a guarantee that the system will violate design principles and rapidly turn that vital business enabler into a business inhibitor. Refactor First is the daily practice that prevents this decay.
Article content

Automated testing is the crucial safeguard. It is why the Testing track is equally important in Agile software systems. When performed with a safety net, refactoring provides clear benefits:

  • Improves Objective Measures: It optimises measurable qualities of the code (e.g. reduces duplication, lowers coupling, increases cohesion), making the code easier to maintain.
  • Enforces Design Principles: It ensures the codebase is consistently aligned with fundamental principles like SOLID and DRY.
  • Favours Platform Emergence: By consistently adhering to principles, refactoring naturally favours the emergence of reusable design elements and code modules. This shift allows teams to develop platforms that run the business, rather than creating separate, specific programs for every new purpose.

What Refactoring Looks Like

Refactoring is when the code structure is changed without changing its external behaviour. One is refactoring when one:

  • Renames classes and members (improving clarity).
  • Splits classes or combines classes (adjusting cohesion/responsibility).
  • Moves a split class to another layer in the application (improving architectural separation/coupling).
  • Splits methods or combines methods (improving method size and responsibility).
  • Restructures a class hierarchy (improving polymorphism/Liskov Substitution).
  • Replaces one data structure with another (improving performance or clarity).
  • Replaces one algorithm with another (improving performance or complexity).

The Refactor-First Discipline

The code must adhere to all design principles after a refactor. Code is usually refactored to “prepare the ground” before new functionality is laid down, ensuring that after the new code is added, the design principles are still present.

This refactor-first approach is the daily discipline that separates emergent design from hacking. It is a continuous activity, which, when coupled with significant design skill, actually minimises the amount of refactoring needed over the system’s lifetime.

Article content

Simple Design establishes the goal, the Rules of Simplicity provide the objective measure to determine if that goal has been met. These criteria act as the ultimate definition of high-quality, maintainable code, ranked by priority:

1. Code is Verified by Automated Tests

The code must fulfil all requirements and perform its intended function. Most importantly, all automated tests must pass, confirming reliability and functionality.

2. Code Contains No Duplication (DRY)

This rule eliminates redundancy in logic and knowledge. Each piece of knowledge exists in exactly one place, including the duplication of concepts, not just code.

3. Code Expresses Each Distinct Idea Separately

The code must communicate its purpose clearly. This means:

  • Names are meaningful.
  • The design makes the intent obvious to other developers.
  • The code is readable and understandable.

4. Fewest Elements

The design uses the minimal number of classes, methods, and lines of code possible. This involves having no unnecessary abstractions or complexity, and removing anything that doesn't serve the first three, higher-priority rules.

Strategic purposes of Rules of simplicity

The Rules of Simplicity serve high-level, strategic objectives for the business:

  • Reduce Complexity: Combat the natural tendency toward over-engineering and keep codebases manageable as they grow.
  • Enable Change: Simple code is significantly easier to modify and extend when requirements change, directly supporting business agility and iterative improvement.
  • Improve Communication: The code itself becomes living documentation, which enables change, reduces friction and onboarding time for new team members.
  • Increase Velocity: Less code means less to test, debug, and maintain, allowing teams to move faster with simpler, more robust systems.

The Tactical purposes of Rules of simplicity

On a day-to-day level, these rules provide clear guidelines for the development team:

  • Decision-Making Framework: When facing design choices (e.g., "Should I add this abstraction?"), the rules provide clear, objective criteria to follow.
  • Refactoring Guide: After tests pass, developers use the rules (especially #2, #3, and #4) as a systematic checklist for improving the design.
  • Code Review Standard: The rules establish objective criteria and a shared language for evaluating code quality and design discussions.

Article content

While developers handle local design decisions as part of their routine work—emphasising that design is pervasive, ongoing, and done by all—some decisions have far-reaching architectural consequences. It is crucial that all developers possess the design skill to recognise when a decision moves from being a tactical choice to a strategic one.

Agile is not about no design; it is about continuous design rather than big up-front design.

For strategic decisions, the team must employ just-in-time design via quick design sessions. Two or, preferably, more developers should meet to discuss and align on the proposed path. These sessions address strategic design issues, contrasting with refactoring, which deals with local design improvements.

To make these discussions effective, apply these two guidelines:

  • Consider multiple credible alternatives: Discuss several, preferably three or more, credible alternatives. This process allows for deeper insight into simplicity and conceptual integrity when choosing the final option.
  • Assess alternatives with concrete scenarios: Assess each alternative based on concrete and specific scenarios. You should consider alternatives that represent the “four corners of the room”: the simplest case, the most complicated case, the longest case, the best average case, and so on.

Successful, emergent design requires high-fidelity team interaction using low-fidelity, transient artefacts (like whiteboards or simple sketches). This reinforces Agile Principle 6: The most efficient and effective method of conveying information to and within a development team is face-to-face conversation.

Article content

CRC cards (Class-Responsibility-Collaborator) is a technique often employed during quick design sessions. Two or more team members list the most important objects, concepts, events, or transactions involved in the feature onto simple index cards. To each card are added its responsibilities (what it does) and its collaborators (who is does it with).

The design is then validated using the plausible scenarios as described in quick design sessions.

While the information on these cards is similar to what you'd find on a class diagram, the key difference is their physical, index-card nature. This tactile nature forces a more human, conversational, and collaborative interaction, which is highly efficient for refining a shared understanding of the solution. Agile Principle 6 (face-to-face conversation) comes to mind again.

Conclusion

We began by defining the difference between code that enables and code that inhibits. The reality is that correctly engineering software systems is not merely a technical exercise; it is a powerful business tool. By adhering to the Agile Design Track, practicing Refactor-First, and diligently applying principles like SOLID and DRY, we transform our source code into the malleable, adaptable clay the modern digital business requires.

This code plasticity is the key enabler for a business to have software systems that sustain and accelerate its agility. It ensures that the enterprise can quickly, safely, and cheaply respond to market changes, delivering new features and fixing bugs without the compounding decay of technical debt.

Ultimately, the commitment to architectural discipline yields a far greater reward than mere stability: it restores the intrinsic satisfaction of the craft. Crafting such systems is a pleasure and joy, because every local change contributes to a robust, coherent global vision. And when the code is clean, expressive, and predictable, the ongoing maintenance and evolution become painless. Running such systems is a pleasure and joy, freeing teams from the relentless grind of firefighting and allowing them to focus on innovation and value delivery.

The choice is simple: will you allow tactical myopia to forge a costly liability, or will you commit to the architectural vision that creates a strategic, adaptable asset? The future of your business rests on the plasticity of your code.

To view or add a comment, sign in

More articles by Keith Crompton

Others also viewed

Explore content categories