20 Essential Software Architecture Principles Every Engineer Should Master
This article presents a concise overview of twenty core software architecture principles—from Dependency Inversion and Separation of Concerns to KISS and Incremental Development—explaining each concept, its purpose, and providing Java code examples to illustrate practical application.
Software architecture is built on a set of fundamental principles that experienced architects use to create maintainable, flexible systems. Below is a quick overview of twenty core principles commonly followed.
1. Dependency Inversion
This principle states that dependencies should point toward abstractions rather than concrete implementations. By inverting the direction of control, runtime dependencies flow from abstractions, reducing direct coupling.
2. Separation of Concerns (SoC)
Systems should be divided according to the type of work they perform—such as business logic, infrastructure, or UI—so that development, testing, and deployment become easier. SoC drives many architectural styles like Domain‑Driven Design, Hexagonal, and Clean Architecture.
3. Inversion of Control (IoC)
IoC extends the idea of Dependency Inversion to the overall control flow, allowing frameworks (e.g., Spring) to manage object creation and lifecycle instead of the application code.
4. Dependency Injection
Dependencies are supplied at runtime, typically via constructors. The following example shows an Action interface injected into a Human class to decide which concrete action runs.
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;
}
public void do() {
action.do();
}
public static void main(String[] args) {
Human human = new Human(new HumanAction());
human.do();
}
}5. Single Responsibility
Each module, class, or function should have only one reason to change, focusing on a single responsibility.
6. DRY (Don’t Repeat Yourself)
Avoid duplicating code; reuse existing functionality instead of copying it across multiple places.
7. Open‑Closed Principle
Software entities should be open for extension but closed for modification, allowing new behavior without altering existing code.
8. Persistence Ignorance
Business logic should remain independent of any specific database or persistence technology, enabling easy swapping of storage mechanisms.
9. YAGNI
“You aren’t gonna need it” discourages premature optimization and over‑engineering of features that may never be used.
10. Boy Scout Rule
Leave the codebase cleaner than you found it; refactor when encountering anti‑patterns.
11. Liskov Substitution
Objects of a subtype must be replaceable for objects of the supertype without altering program correctness.
12. Encapsulation
Hide internal details of a component by restricting external access, typically using private members.
13. Loose Coupling
Dependencies between modules should be minimal so that changes in one part have little impact on others. Techniques include Dependency Inversion, asynchronous messaging, and event sourcing.
14. Cohesion
Elements within a module should be highly related, supporting single responsibility and loose coupling.
15. Interface Segregation
Clients should not be forced to depend on interfaces they do not use. This principle mainly applies to statically‑typed languages.
Example of a violation in a Java service layer:
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();
}
}16. Bounded Context
In Domain‑Driven Design, a bounded context isolates a specific domain model, allowing each context to evolve independently with its own terminology and data store.
17. Stable Dependencies
Modules should depend only on reliable, stable artifacts—e.g., trusted Docker images or vetted libraries.
18. Polymorphism
One of the four pillars of OOP; it enables objects to be treated through a common interface while exhibiting different behaviors.
19. Modularization
Dividing a system into independent modules is a concrete form of the Single Responsibility principle applied at the architectural level.
20. Abstraction
Focus on essential characteristics by hiding irrelevant details, similar to generalization.
21. KISS (Keep It Simple, Stupid)
Encourage simple, easy‑to‑understand code to reduce misunderstandings.
22. Incremental/Iterative Approach
Agile development builds software in small, functional increments, delivering value continuously.
23. Least Knowledge (Law of Demeter)
Modules should only know what they need to, limiting the spread of information and reducing coupling.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
