23 basic principles of software architecture

2024.04.28

Software architecture is based on a set of basic principles that apply to all kinds of software systems. An experienced architect knows these principles and can implement specific principles in the right place in the software product. Here’s a quick look at the basic principles architects follow on a daily basis:

1. Dependency Inversion

This principle states that the direction of dependencies should be abstract rather than implementation specific. If a compile-time dependency flows in the direction of run-time execution, a direct dependency is formed. With dependency inversion, you can reverse the direction of dependency control. The following article discusses this principle in more depth: How to apply SOLID Software Design Principles to Spring Boot Application (Part 5)[2]

2. Separation of Concerns

This principle states that software systems should be divided according to the type of work they do. For example, it can be divided into different parts according to business logic, infrastructure or user interface. Makes development/testing/deployment easier by dividing the system into different parts based on different activity areas. SoC is the driving force behind software architecture patterns such as domain-driven design, hexagonal architecture, and clean architecture.

3. Inversion of Control

This principle is similar to the dependency inversion principle, but applies in a wider context. IoC inverts the control flow managed by different third-party frameworks such as Spring Framework. Unlike traditional Java EE programs (where Beans are initialized programmatically by development engineers), Spring controls the configuration of beans, which means an inversion of control.

4. Dependency Injection

This principle means that dependencies should be injected at runtime through the constructor. In the following example, the Action Interface is injected into the Human class through HumanAction Implementation to determine which specific action is implemented at runtime. This technique provides flexibility in controlling dependencies:

package az.alizeynalli.di;

public interface Action {
    void do();
}

public class HumanAction implements Action {
 
    @Override
    public void do() {
        System.out.print("run");
    }
}

public class Human  {
     
    Action action;
     
    public Human(Action action) {
        this.action = action;
    }
 
    @Override
    public void do() {        
        actoin.do();        
    }
}

    public static void main(String[] args) {
        Human human = new Human(new HumanAction);
        human.do();
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

5. Single Responsibility

The main idea of ​​this principle is to limit each building block of a software system to a unique responsibility. No matter what the scope of the building block is, whether it is a plugin, a package, a class, a function, or even a variable, there should be only one responsibility. This article discusses this principle in more depth: How to apply SOLID Software Design Principles to Spring Boot Application (Part 1)[3]

6. DRY(Don’t Repeat Yourself)

This principle aims to eliminate redundancy by avoiding duplication of code. If there is existing functionality for some behavior, it should be reused rather than copying the same piece of code across multiple instances.

Every piece of knowledge must have a single, unambiguous, authoritative representation in the system.

7. Open-Closed Principle (Open-Closed)

Software components should be open for extension and closed for modification.

A simple description of this principle was first proposed by Bertrand Meyer. A software system that needs to be modified every time will only become a mess, and such a messy program is prone to errors every time it is modified. Each new feature should maximize new code and minimize changes to old code. Ideally, there should be zero changes to old code.

8. Persistence Ignorance

The idea of ​​persistence transparency is that the code should not be affected by any database or persistence technology. Business logic should be agnostic to any technology. If tomorrow, there were better, more efficient, cheaper persistence technologies, it should be possible to change this part of the system in a way that doesn't affect the upper abstractions.

9. YAGNI

You ain't gonna need it. This principle attempts to avoid premature optimization of software systems. Developers often over-engineer something into a system in the hope that it will help at some point in the future, but that moment often never comes.

10. Boy Scout Rule

Leave the campsite cleaner than when you arrived.

The main idea here is that when developing and encountering anti-patterns, insist on refactoring the code. Over time, this improves code quality.

11. Liskov-Subsititution

If for every object o1 of type S, there is an object o2 of type T, such that for all programs P defined with T, the behavior of P does not change when o1 replaces o2, then S is a subtype of T .

This definition from Barbara Liskov may sound confusing, but at its core the principle is simple and easy to understand. If you restate the definition above, this principle means: When using inheritance, the hierarchy of inheritance should be consistent in terms of functionality and business logic. Subclasses should be interchangeable and should not change the behavior of the parent class. As a simple example, consider the "infamous square/rectangle" problem. where square should not be a subtype of rectangle because the definitions of height and length are different for the two geometric shapes (the height and length of a square are equal, while the height and length of a rectangle are different).

12. Encapsulation

The different building blocks of a software system should be encapsulated to limit outside access to their components. This can be achieved by setting the components as private within the class scope or by setting access restrictions at the plugin scope (in the case of Java), thus hiding the information.

13. Loose Coupling

One of the most important principles in software architecture is loose coupling, which states that the dependencies of a software system should be loose, and changes in one part of the system should have minimal impact on other parts. Loose coupling can be achieved through dependency inversion, asynchronous message middleware, event sources, etc. The following article provides an in-depth look at different forms of coupling in software engineering: 9 Forms of Coupling in Software Architecture[4]

14. Cohesion

Cohesion refers to the degree to which elements within a module are dependent on each other. In a sense, it is a measure of the strength of the relationship between the methods and data of a class and some unified purpose or concept that the class serves.

Building highly cohesive classes is a best practice, which facilitates the implementation of the single responsibility principle, loose coupling, etc.

15. Interface Segregation

The interface segregation principle states that clients should not be forced to rely on methods they do not use.

It should be clear that this principle mainly applies to statically typed programming languages, such as Java, C, etc. In dynamically typed languages ​​like Python or Ruby, this principle doesn't make much sense.

You can imagine a situation where both our Income and Expense use cases rely on business logic functions that support these two use cases. Therefore, many dependencies of the Income use case are related to the Expense use case, and the dependencies of the Expense use case also have the same problem. Based on the above discussion, the ISP violations are as follows:

package az.alizeynalli.cashflow.core.service;

public interface ConverterService {
    Income convertIncome(Income income);
    Expense convertExpense(Expense expense);
}

@Component
public class ExpenseConverterServiceImpl implements ConverterService {

    @Override
    public Income convertIncome(Income income) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Expense convertExpense(Expense expense) {
        // convert expense here
        return expense;
    }
}

@Component
public class IncomeConverterServiceImpl implements ConverterService {

    @Override
    public Income convertIncome(Income income) {
        // convert income here
        return income;
    }

    @Override
    public Expense convertExpense(Expense expense) {
        
        throw new UnsupportedOperationException();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.

16. Bounded Context

Bounded context is a central pattern of domain-driven design. Provides a way to deal with complexity by decomposing a large application or organization into separate conceptual modules. Each conceptual module represents a context that is separate from other contexts (and therefore bounded) and can evolve independently. Ideally, each bounded context should be free to choose its own names for the concepts within it, and should have exclusive access to its own persistence store. [5]

17. Stable Dependencies

This principle states that the different building blocks of a software system should rely only on reliable, stable artifacts. This principle makes more sense in docker image terminology, when we import different dependencies from docker hub without even knowing if they are reliable/stable.

18. Polymorphism

This actually falls under the 4 pillars of object-oriented programming, which encourages the use of interfaces that can be provided in multiple forms, and polymorphism means entities with multiple forms.

19. Modularization

Modularization is the process of dividing a software system into independent modules, each of which works independently. This principle is another form of the Single Separation of Responsibilities principle applied to the static architecture of software systems.

20. Abstraction

This also belongs to the four pillars of object-oriented programming:

Removing physical, spatial, or temporal details or attributes when studying an object or system to focus on more important parts is essentially similar to the process of generalization.

21. KISS(Keep It Simple, Stupid)

Taken literally, this principle motivates engineers to keep their code simple and stupid (easy to understand) to avoid misunderstanding by others.

22. Incremental/Iterative Approach

This principle is the basis of the Agile Software Development Manifesto and is based on the idea that software systems should be developed in an incremental and iterative manner, with each iteration increasing the functionality of the system and ensuring its operation.

23. Least Knowledge Principle (Least Knowledge)

Or information envying, another term for the encapsulation or information hiding principle, which states that different parts of a software system should only have the knowledge they need.