← Back to Tech · Português

Software Engineering Principles & Patterns

A direct technical guide from foundational principles (KISS, DRY, YAGNI and SOLID) to design patterns and architectural patterns (MVC, MVVM and Clean Architecture), including quality metrics, a review checklist, code examples and common pitfalls — all aimed at building maintainable, reliable, secure and efficient systems.

1) What are principles and patterns?

They are established solutions to recurring software problems. They are not libraries you install, but organizational recipes: tested ways to structure code so it remains easy to read, test, change and reuse. There are three broad levels, from general to concrete:

2) Foundations: KISS, DRY and YAGNI

Before sophisticated patterns, three simple principles prevent much of the avoidable complexity in software:

3) SOLID — the five object-oriented principles

SOLID is an acronym for five principles popularized by Robert C. Martin. Their purpose is to keep code flexible, decoupled and testable, making future change cheaper.

LetterPrincipleIn one sentence
SSingle ResponsibilityA class should have only one reason to change.
OOpen/ClosedOpen for extension, closed for modification.
LLiskov SubstitutionSubtypes should replace their base type without breaking expected behavior.
IInterface SegregationSeveral focused interfaces are better than one overly broad interface.
DDependency InversionDepend on abstractions, not concrete implementations.

Example: Single Responsibility (SRP)

Before — the class calculates, persists and sends email:

class Order:
    def calculate_total(self): ...
    def save_to_database(self): ...   # persistence responsibility
    def send_email(self): ...         # notification responsibility

After — each class has one reason to change:

class Order:
    def calculate_total(self): ...

class OrderRepository:
    def save(self, order): ...

class Notifier:
    def send_email(self, order): ...

Example: Dependency Inversion (DIP)

High-level code depends on an abstraction, so replacing the delivery service does not require changing business logic:

class DeliveryChannel:
    def send(self, message): raise NotImplementedError

class Email(DeliveryChannel):
    def send(self, message): print("email:", message)

class SMS(DeliveryChannel):
    def send(self, message): print("sms:", message)

class Notifier:
    def __init__(self, channel: DeliveryChannel):
        self.channel = channel
    def notify(self, message):
        self.channel.send(message)

4) Design Patterns (Gang of Four)

The 23 classic patterns from Design Patterns (1994) fall into three families. You do not need to memorize them. Learn the common ones and apply them when the problem actually appears.

Creational — how objects are created

Factory MethodAbstract FactoryBuilderSingletonPrototype

Factory: centralizes object creation so callers do not need to know which concrete class is instantiated.

def create_transport(kind):
    factory = {"car": Car, "ship": Ship, "plane": Plane}
    if kind not in factory:
        raise ValueError("Invalid transport")
    return factory[kind]()

Structural — how objects are composed

AdapterDecoratorFacadeProxyComposite

Adapter makes incompatible interfaces work together. Decorator adds behavior to an object without altering its class.

Behavioral — how objects collaborate

StrategyObserverCommandStateIterator

Strategy allows an algorithm to change at runtime:

class Shipping:
    def __init__(self, strategy):
        self.strategy = strategy
    def calculate(self, weight):
        return self.strategy(weight)

standard = Shipping(lambda w: w * 2)
express = Shipping(lambda w: w * 5)

Observer automatically notifies subscribers when an object's state changes; it underpins event systems and reactive interfaces.

Layer patterns: Repository and Service

5) Architectural patterns: MVC, MVVM and Clean Architecture

Where design patterns organize object collaboration, architectural patterns organize the system as a whole.

MVC — Model, View, Controller

User → Controller → Model (read/update data)
             ↓
           View (render result)

MVC is used by frameworks such as Spring, Laravel, Ruby on Rails and ASP.NET.

MVVM — Model, View, ViewModel

A variation designed for reactive interfaces. The ViewModel exposes screen-ready data and keeps the View synchronized with the Model through data binding.

Clean / Hexagonal Architecture

The business rules stay at the center and do not know about the database, framework or UI. Dependencies point inward, keeping the core independently testable.

[ Frameworks / UI / Database ] → [ Adapters ] → [ Use cases ] → [ Entities ]
       replaceable outside                                  stable core

Layered Architecture

Organizes the application into horizontal layers — usually presentation, domain/business and persistence — with controlled dependency direction.

Microservices

Split a system into independently deployable services. This can improve scaling and team autonomy, but adds operational and distributed-systems complexity. Poorly controlled coupling merely replaces spaghetti code with distributed spaghetti architecture.

6) Comparison: when to use each

PatternProblem addressedUse when…
SOLIDRigid code that is expensive to changeAs a baseline for object-oriented design.
FactoryScattered, coupled object creationSeveral concrete classes implement the same role.
StrategyMany if/else branches selecting algorithmsBehavior must vary at runtime.
ObserverObjects need to react to changesEvents, notifications and reactive UI.
RepositoryData access mixed into business logicYou want to isolate storage from the rest of the code.
MVC / MVVMUI and business rules are entangledApplications with web, mobile or desktop interfaces.
Clean ArchitectureHeavy dependence on framework/databaseLarge or long-lived systems.
Layered / MicroservicesNeed clearer system boundaries or independent deploymentChoose layered organization for simpler separation; microservices only when independent deployment and scaling justify the cost.

7) Practical impact

8) Metrics for tracking quality

PrincipleBenefitMetrics / signalsTools
KISSReadability, fewer defectsLow cyclomatic complexity (often aim for <10 per routine)ESLint, Pylint, SonarQube
DRYLess rework and inconsistencyDuplication percentage, code smellsSonarQube, PMD
YAGNIFaster value deliveryDead code, unused featuresBacklog review, usage metrics
SOLIDExtensible, testable architectureCohesion/coupling, oversized classes, cascading changesSonarQube, ArchUnit
TDD / testingReliability, regression preventionCoverage, failure rate, suite durationpytest, JUnit, Jest, CI/CD
Secure by DesignFewer vulnerabilitiesSAST/DAST findings, OWASP Top 10 issuesOWASP ZAP, CodeQL, Dependency Check

9) Quick review checklist

10) Code examples: modularity and security

Python — modularity + error handling

def calculate_product(value):
    return value * 0.9

def process_order(kind, value):
    calculators = {"product": calculate_product, "service": lambda v: v}
    if kind not in calculators:
        raise ValueError("Invalid type")
    return calculators[kind](value)

Java — protection against SQL injection

PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM users WHERE login = ?"
);
stmt.setString(1, user);
ResultSet rs = stmt.executeQuery();

11) Common pitfalls

12) Resources

Summary: master the principles first, measure quality with objective signals, recognize design patterns when the problem actually calls for them, and choose architecture in proportion to system size. Patterns are tools, not goals; the end state should be simple, modular, testable, secure and change-friendly code.

← Back to Tech