Why is Software Architecture Important?
Software architecture reflects the internal quality of the product compared to the user interface, which reflects the external marker.
I have been in the software industry for more than 15 years, and I got to work on various applications and the architecture/building blocks that empower those applications. A few were enterprise applications, while the others were greenfield applications to empower tech startups. However, for all of them, the architecture, design principles, and decisions made around them played a vital role in making it a success from a technological standpoint.
Every application/feature is financed by its business stakeholders, targeted at a specific consumer base, and developed by a team of architects, developers, and testers. Different stakeholders of the application have other priorities. From the perspective of business stakeholders, time to market is essential. For consumers, the application must solve the purpose on time and intuitively when I think from the perspective of the development team who owns the application from a development perspective. In that case, delivering the product/feature with the best quality possible in the agreed-upon time is essential.
What is visible to the application's consumer are the features and the user interface displaying those features intuitively. They look at the application from the perspective of user experience, possible use cases they want to use it for, how intuitive the UI is, and the usability aspects of the application, but what is invisible or hidden is the architecture or the code that is running in the background to empower this intuitive interface. Let me put forward different aspects of the good or bad quality of the code and its relevant impact on the business or consumer stakeholders.
When can we say a code is a good quality code?
Self-documenting code
The name of the package/folder, class, method, and variables should be well-defined and give a real sense of what it is meant for. This way, we can relate it to a real-world entity, purpose, or attribute while reading. Comments should be added wherever required to support the logical flow and improve readability.
Easy to read and understand
A few techniques that we should use to write easy-to-read and understand code are:
- A class or a script should cater to one single purpose.
- An extended method should be broken down into smaller methods.
- Method (s) should have a logical name.
- An argument should have a logical name, and the same name should be used throughout its traversed path to this particular method.
Modularisation
This is about dividing the code into a proper structure so we can understand what to expect from that piece of code by looking at the folder/package structure. We should write the code in the right place. For example, it is incorrect to have code related to payment within the customer module or stuff related to purchase within the payment module.
Reusability
When we break a long method into smaller logical methods, we will discover that quite a few of those methods are reusable. The reusability of the code might happen at the same class level or across different classes. Sometimes, we shift the standard reusable code to another layer, like a framework or utils. It depends on whether the standard common code is implicit in the feature in development or is common and should be moved towards the framework/boilerplate.
Easy to enhance
An application’s inherent nature is to enhance and evolve according to the input from the business, changing business dynamics and use cases based on consumer behaviour. Whatever we have written today must be written to enhance the logical flow of the code later with the minimum of rewrite. This is only possible if the code is well-written and not too clumsy and cluttered.
Single Responsibility
A method, function, or class should always solve one purpose simultaneously. If our scope increases while enhancing or writing, which might cater to multiple purposes, we should refactor and break it down. This will help us retain code quality and avoid jumbled-up code.
Well-defined method/API signature or contract
One of the most costly parts is changing the signature/definition of the method or the API, as other application flows might have already consumed it. We should take a futuristic approach to ensure the signature we are finalising does not require any change. If a change is necessary, we should do it in a backward-compatible way and support it with an API version change if required.
Domain objects should align with the current organisational state and future vision.
Applications/features are developed to empower businesses to achieve their goals and for the consumers to fulfil their needs as desired by the product. An application mimics the offline or manual workflow that, in most cases, exists. So, the class, script, method, variables, API signature, and entities should all follow the semantics of the domain and the business for which it exists. While designing an application, the more our code structure aligns with the business's current and future state, the more it would be to enhance it as per changes in market dynamics.
Data structure
A well-thought-out data structure might require some effort in the initial phase, but it pays off in the long term. It is a fundamental building block in the overall architecture. Investing time in a data structure in the initial phase helps enhance the application to cater to the new business needs by improving rather than redesigning the existing structure.
Readily scalable
The application load usually keeps increasing because of the business territory's scale or new consumer adoption. With new features coming in or with enhancement within the existing features, a product moves towards maturity and solves more and more use cases for which a consumer might use. A good application is always ready for that scale. It can be only achieved by:
- iterative refactoring of the code
- giving equal priority to technical debt
- doing regular performance load benchmarking for 2X & 5X load
Low cost to change
An excellent attribute for gauging the quality of the code and the architecture is the cost incurred in enhancing an existing feature. Good quality code incurs less cost, is more agile, and helps to keep infra cost in control.
Fewer bugs
Well-written code undergoes unit test cases, integration testing, static code review, vulnerability testing, security testing, and code coverage checks. It is expected to be maintained as part of the development life cycle and ensures code quality at the development and deployment stages (CD/CI).
Clean code
We should have linting in code to ensure proper code syntax and code written according to certain rules and guidelines.
We can have git hooks to validate links and control code quality and cleanliness.
Indicators of poor code quality
Hard to understand code
Code should always be written so that anyone in the team with a proper business context can read and understand it and its logic. Code tends to lose its quality semantics when we try to publish it without a proper review cycle or when it is not self-documented.
High bug rates
Chances are high that the probability of bugs surfacing with poor-quality code increases with every release or change induced.
In simple words, it's risky to touch poorly written code as it might break something.
Poor performance
A slow API or a flow is another indicator of poor code quality. The slowness might be due to the following:
- missed or wrong indexes at the DB level
- Poorly handled computing or memory consumption due to how the code is written.
- Threads, if not managed properly
- Or due to an external component in the pipeline, i.e., Redis, etc.
High maintenance cost
The maintenance cost of an application goes high with time if we ignore the following:
- regular cleanup
- Work on technical debt
- code refactoring
- Re-modelling wherever required
It leads to bulky, unused and slow code.
How can we avoid poor code quality?
Code review
This should be treated as an essential step in the development life cycle. We should ensure that whatever a developer is doing should be done as part of the feature branch and get merged into the release branch only after two review cycles and the bug fixes are done for the same. Also, we should ensure that the build on the server should happen via the release branch only.
Code cleanup
As we move ahead with the new releases, we are sometimes left with new/legacy code for which the more recent version has been released. We should regularly plan to clean up this code. One way to do this is to create cleanup tickets with relevant comments and align them with either future releases or as part of the backlog.
Business agility should not compromise code quality.
At times, we have to deliver under time constraints. Both time to market and code quality are essential. We can achieve this by thinking from the perspective that not everything needs to be developed and delivered as part of the first launch. We can develop the feature/product into phases by defining the scope well, which will help us achieve the same along with quality.
Keep track of technical debt.
Technical debts are unavoidable. They will always exist in the system. The important thing is that we should keep track of them. We should keep them adding to the backlog with pertinent details and prioritise them as part of the product roadmap and the features as part of the sprint.
Monitoring and Alerts
We should publish success, error, audit and action logs to either in-house or third-party tools concerning:
- touchpoints
- intermittent flows (capturing traceability)
Create a dashboard to analyse trends for different:
- feature
- flow
- sub-systems
- systems
It should be analysed from different aspects like:
- business aspects
- behavioural aspects
- technical aspects
- cost aspects
- performance aspects
This system should raise alerts for the respective audience in case of deviations in trends, error scenarios, or unusual behaviours.
Start with MVP
While planning for a new feature, we should always start with an MVP (Minimal Viable Product):
- Do a soft launch
- Build observation around consumer behaviour.
- Look at the usability patterns.
- Generate insight from the matrix.
- Evaluate success/failure criteria.
- Plan the next set of enhancements.
What should we do to ensure good quality code?
Invest in bootstrap/framework code
More time and effort will be invested in the initial phase of the development to carve the building blocks of the architecture, mainly around NFRs, the faster and smoother the application development will be at the later stage of the product.
A balance between time to market versus code quality
We should maintain time to market to achieve the best possible product and vice versa. It is essential to balance the two. If we focus on the quality of the product, we might get delayed with the delivery timelines, and if we focus on the timeline, we might compromise on the code quality. A mix of both is an excellent approach to move ahead with.
Domain knowledge
What might look technically correct might not be accurate when we look at it from the perspective of business or domain. Domain knowledge adds a level of refinement to the application architecture.
Current and future state, knowledge of roadmap
The better a team understands the current and future state of the product/organisation and the goals we want to achieve, the better we will be aligned to achieve those targets. A solution is not just right or wrong. Whether it is right or wrong, changes are very subjective, based on the requirements and goals we want to achieve. What might be right today might not be correct in the future scenario.
Think ahead of time
We should think ahead of time; it's a world full of uncertainty, and we should zero down on assumptions as much as possible. As we move forward with the development lifecycle, many things unfold. They are sometimes big enough to question a critical solution we agreed upon earlier.
The cost of developing a feature versus its quality is counterintuitive. Good-quality code can’t be achieved with a compromised timeline/cost of delivery.