Phase 10: Software Architecture
- Authors

- Name
- Wan Ilhami
- @wan-ilhami-43515a184
The Absolute Beginner's Guide to Software Architecture
Imagine you are asked to build a doghouse. You probably don't need a blueprint. You grab some wood, a hammer, some nails, and within a few hours, you're done. If you make a mistake, it's easy to fix.
Now imagine you are asked to build a 100-story skyscraper. If you take the same approach—just grabbing steel and welding it together without a plan—the building will collapse under its own weight before you even reach the 10th floor.
Writing software is exactly the same. When you are writing a tiny script to rename some files, you don't need architecture. But when you are building an application that will be used by thousands of people, maintained by dozens of developers, and modified over several years, a lack of architecture will result in "Spaghetti Code"—a tangled mess where fixing one bug introduces three new ones.
Software Architecture is the blueprint of your application. It defines how different parts of your code communicate, how data flows, and how the system can grow without breaking.
1. The Foundation: The SOLID Principles
Before we look at large-scale architectures, we must look at how we write individual classes and functions. In the early 2000s, Robert C. Martin (known as Uncle Bob) popularized five principles of Object-Oriented Programming that lead to clean, maintainable code. They form the acronym SOLID.
S: Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
Imagine a restaurant employee who is expected to greet customers, cook the food, wash the dishes, and do the accounting. If the tax laws change, this employee has to change how they work. If the menu changes, they have to change. They have too many responsibilities.
In code, a class should do exactly one thing.
Bad Example:
class UserProfile {
getUserData() { /* fetches from DB */ }
formatUI() { /* creates HTML */ }
sendWelcomeEmail() { /* connects to email server */ }
}
If the email server changes, this class breaks. If the database changes, this class breaks.
Good Example:
class UserRepository {
getUserData() {} // Only handles database
}
class UserPresenter {
formatUI() {} // Only handles UI
}
class EmailService {
sendWelcomeEmail() {} // Only handles email
}
O: Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
You should be able to add new functionality to a system without rewriting existing, working code.
Bad Example:
class PaymentProcessor {
process(type: string, amount: number) {
if (type === 'credit_card') { /* ... */ }
else if (type === 'paypal') { /* ... */ }
// If we want to add Bitcoin, we have to modify this existing function!
}
}
Good Example:
interface PaymentMethod {
processPayment(amount: number): void;
}
class CreditCard implements PaymentMethod {
processPayment(amount: number) { /* ... */ }
}
class PayPal implements PaymentMethod {
processPayment(amount: number) { /* ... */ }
}
// Now, to add Bitcoin, we just create a new class. We don't touch existing code!
class Bitcoin implements PaymentMethod {
processPayment(amount: number) { /* ... */ }
}
L: Liskov Substitution Principle (LSP)
"Subclasses should be replaceable for their base classes without breaking the app."
If it looks like a duck and quacks like a duck, but needs batteries, you probably have the wrong abstraction. A subclass must behave exactly like its parent class is expected to.
I: Interface Segregation Principle (ISP)
"Don't force classes to implement methods they don't use."
Imagine buying a standard printer. The manufacturer forces you to sign a contract stating you must also know how to scan and fax, even though the machine only prints.
Bad Example:
interface SmartDevice {
print(): void;
fax(): void;
scan(): void;
}
// A simple printer has to implement fax and scan, even if it leaves them empty!
class SimplePrinter implements SmartDevice {
print() { console.log("Printing"); }
fax() { throw new Error("I can't fax"); }
scan() { throw new Error("I can't scan"); }
}
Good Example:
interface Printer { print(): void; }
interface Scanner { scan(): void; }
class SimplePrinter implements Printer {
print() { console.log("Printing"); }
}
D: Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
When you buy a lamp, you plug it into a standard wall socket. The lamp doesn't care if the electricity comes from a coal plant or solar panels. It just relies on the "Interface" of the wall socket.
Your code should do the same. A business logic class should not rely on a specific MySQL database class; it should rely on a generic DatabaseInterface.
2. High-Level Architectural Patterns
Once your individual classes are SOLID, how do you arrange thousands of them into an application?
The Traditional: Layered (N-Tier) Architecture
This is the most common and intuitive way to build backend software. You divide your application into horizontal layers, like a cake.
- Presentation Layer (Controllers): Handles HTTP requests, reads JSON, and sends HTTP responses.
- Business Logic Layer (Services): The "brain" of the app. Calculates taxes, checks if a user is banned, etc.
- Data Access Layer (Repositories): Writes SQL queries to talk to the database.
Rule: A layer can only talk to the layer directly beneath it. The Presentation layer cannot talk directly to the Data layer.
The Modern Standard: Clean / Hexagonal Architecture
Layered architecture has a flaw: the Business Logic layer depends heavily on the Data layer. If you decide to switch from MongoDB to PostgreSQL, your business logic might break.
Clean Architecture (also known as Ports and Adapters or Hexagonal Architecture) flips this around using the Dependency Inversion Principle.
- The Center (Domain): The pure business rules. No frameworks, no HTTP, no SQL. Just pure TypeScript/JavaScript objects.
- The Outside (Infrastructure): The database, the web framework (Express/NestJS), external APIs.
The outside world plugs into the center via "Ports" (Interfaces). This means the core logic is entirely isolated. You can run your entire business logic in a test suite without a real database or a web server!
The Enterprise Behemoth: Domain-Driven Design (DDD)
DDD is not for simple CRUD (Create, Read, Update, Delete) apps. It is meant for incredibly complex business domains (like banking software or global logistics).
Instead of thinking about "Databases" and "Controllers", developers and business experts work together to model the real world.
- Entities: Objects with a distinct identity (e.g., a
Userwith an ID). - Value Objects: Objects defined by their attributes, with no distinct identity (e.g., a
CurrencyAmountof $50). - Bounded Contexts: Recognizing that the word "Product" means something entirely different to the "Shipping Department" than it does to the "Sales Department", and splitting the software accordingly.
Summary
Architecture is about managing complexity. By following SOLID principles and adopting structured architectures like Clean Architecture, you ensure that your code remains flexible, testable, and pleasant to work with, whether you have 1,000 lines of code or 1,000,000.