SOLID Principles
SOLID is an acronym for five design principles intended to make software designs more
understandable, flexible, and maintainable.
Principle Name Explanation with Real-World Example
Single A class should have only one reason to change,
S Responsibility meaning it should only have one job.
Principle (SRP)
Example: Imagine a Report class. If it's responsible for generating the report content
and printing the report, it violates SRP. If the printing method changes (e.g., from PDF to
HTML), you have to change the Report class, even if the content generation logic
remains the same. The solution is to separate the concerns into a ReportGenerator and
a ReportPrinter class.
Open/Closed Software entities (classes, modules, functions, etc.)
Principle (OCP) should be open for extension, but closed for
O modification. You should be able to add new
functionality without altering existing, working code.
Example: Consider a system that calculates the area of shapes. If you have a
ShapeAreaCalculator class and you want to add a new shape (e.g., a Triangle), you
shouldn't have to modify the existing ShapeAreaCalculator class. Instead, you should
define an IShape interface and create a new Triangle class that implements it. The
calculator works on the IShape interface, so it's closed to modification but open to new
shape extensions.
Liskov Substitution Objects of a superclass should be replaceable with
Principle (LSP) objects of its subclasses without breaking the
L application. Essentially, a subclass should extend its
parent class without changing its behavior.
Example: If you have an IBird interface with a Fly() method and a Penguin class
implements IBird. Since penguins can't fly, implementing Fly() to throw an exception or
do nothing violates LSP. Any code that expects an IBird to be able to fly will fail when
given a Penguin. The solution is to refactor the interface, perhaps to ICanFly and IBird
(only containing non-flying behaviors).
Interface Clients should not be forced to depend on interfaces
I Segregation they do not use. This means large interfaces should
Principle (ISP) be split into smaller, more specific ones.
Example: An IWorker interface with methods like Work(), Eat(), and Manage(). If a robot
worker only needs Work(), but is forced to implement Eat() and Manage(), it violates ISP.
The solution is to split it into IWorker, IEater, and IManager. The robot would only
implement IWorker.
Dependency High-level modules should not depend on low-level
Inversion Principle modules; both should depend on abstractions
D (DIP) (interfaces). Abstractions should not depend on
details; details should depend on abstractions.
Example: A BusinessLogic class directly creates and uses a concrete SqlDatabase
class. The high-level BusinessLogic is dependent on the low-level SqlDatabase detail.
To fix this, you define an IDataAccess interface and make both BusinessLogic depend
on IDataAccess and SqlDatabase implement IDataAccess. The dependency is now
"inverted" to an abstraction.
Design Patterns (Experience with 3+)
Design patterns are reusable solutions to common problems in software design. I've
successfully implemented many, including the following:
1. Factory Method (Creational Pattern)
• Concept: Define an interface for creating an object, but let subclasses decide
which class to instantiate. The Factory Method lets a class defer instantiation to
subclasses.
• Real-World Example (Payment Processing): In an e-commerce system, you
might have different payment gateways: CreditCardPayment, PayPalPayment,
CryptoPayment. Instead of having your OrderProcessor class know how to create
each one, you use a PaymentFactory. When processing an order, the
OrderProcessor asks the PaymentFactory to
CreatePaymentProcessor(PaymentType type). The factory handles the logic of
instantiating the correct concrete class (new CreditCardPayment(), etc.),
decoupling the order processing logic from the concrete payment details.
2. Repository Pattern (Structural Pattern)
• Concept: Mediates between the domain and data mapping layers using a
collection-like interface for accessing domain objects. It essentially separates
business logic from data access logic.
• Real-World Example (User Management): A user service needs to fetch user
data. Instead of injecting a specific AppDbContext (EF Core) or raw SQL
connection, you inject an IUserRepository. This interface defines methods like
GetUserById(int id), AddUser(User user), etc. The concrete UserRepository
implementation handles the details (e.g., using LINQ queries against a SQL
database). If you later switch to MongoDB, you just create a new
MongoUserRepository implementing the same interface—the business logic stays
untouched.
3. Observer Pattern (Behavioral Pattern)
• Concept: Defines a one-to-many dependency between objects so that when one
object (the subject) changes state, all its dependents (the observers) are notified
and updated automatically.
• Real-World Example (Stock Ticker): A Stock Exchange is the subject. It updates
its price periodically. Trading Bots, Analytic Dashboards, and Mobile Apps are all
observers. When the Stock Exchange's price changes, it notifies all registered
observers, which then update themselves (e.g., the trading bot executes a trade,
the dashboard updates the chart). This is often implemented in C# using Events
and Delegates.
Dependency Injection (DI)
Dependency Injection is a technique whereby one object (the client) supplies the
dependencies of another object (the service). It's a way to implement the Dependency
Inversion Principle (DIP).
How it Works:
1. Inversion of Control (IoC): The key concept is that the responsibility for creating
an object's dependencies is inverted from the object itself to an external
component (the IoC Container or DI Container).
2. The Container: You configure an IoC Container (like the built-in one in [Link]
Core) to know which concrete class to use when a specific interface is requested
(e.g., "when someone needs IUserRepository, give them a SqlUserRepository").
3. Injection: The container handles the creation of the dependency and injects it into
the client class, typically via the client's constructor (Constructor Injection).
Real-Life Example (Logging Service):
Consider a ProductService that needs to log messages.
Without DI:
public class ProductService
{
private FileLogger _logger = new FileLogger(); // ProductService *creates* its dependency
public void AddProduct(Product p)
{
// ... business logic ...
_logger.Log("Product added."); // Directly calls the concrete logger
}
}
The ProductService is tightly coupled to FileLogger. If you switch to DatabaseLogger, you
must modify ProductService.
With DI (Constructor Injection):
public interface ILogger
{
void Log(string message);
} // Abstraction
public class FileLogger : ILogger
{ /* implementation */
} // Detail
public class ProductService
{
private readonly ILogger _logger; // Depends on the Abstraction
public ProductService(ILogger logger) // Dependency is Injected via constructor
{
_logger = logger;
}
public void AddProduct(Product p)
{
// ... business logic ...
_logger.Log("Product added."); // Calls the Abstraction
}
}
The IoC Container resolves and provides the ILogger implementation. This makes
ProductService decoupled, testable (you can inject a MockLogger for unit testing), and
adheres to DIP.
Asynchronous Communication (Async/Await)
Asynchronous Communication in .NET/C# allows a method to run long-running, non-
CPU bound operations (like I/O requests: database calls, network calls, file access)
without blocking the thread that initiated the call.
How it Works (Avoiding Main Thread Blocking):
1. The Problem: Traditional synchronous I/O operations block the calling thread. The
thread sits idle, waiting for the external resource (like a database) to respond. This
wastes resources, especially in a server environment like [Link] where threads
are needed to process other incoming requests.
2. async and await:
o The async keyword marks a method as one that can contain await
expressions.
o The await keyword is the magic: When a thread hits an await call (e.g., await
_dbContext.[Link]();), the thread is released back to the thread
pool to serve other requests.
o The call to the database (or other I/O operation) is processed
asynchronously by the operating system.
3. The Continuation: Once the awaited operation completes, the runtime (via a
mechanism called a "Synchronization Context" or a TaskScheduler) finds an
available thread to resume the rest of the original async method (the
"continuation"). The entire process appears synchronous to the developer, but the
underlying thread management is highly efficient, maximizing thread pool
utilization and avoiding main thread blocking.
C# Example:
// The thread is released when it hits 'await'
public async Task<List<Product>> GetProductsAsync()
{
// A thread begins execution here.
var products = await _dbContext.[Link]();
// Thread is released to process other requests while DB runs query.
// A different thread may resume execution here once the results are back.
return [Link](p => [Link]).ToList();
}
Microservices
Microservices is an architectural style that structures an application as a collection of
smaller, independent services, rather than a single, monolithic application.
Key Characteristics:
• Independent Deployment: Each service can be deployed independently without
affecting other services.
• Decentralized Data Management: Each service manages its own data store (e.g.,
the Order service might use SQL, while the User service uses NoSQL).
• Technology Heterogeneity: Services can be written in different
languages/frameworks (e.g., a .NET service for user auth, a Python service for ML
processing).
• Resilience/Fault Isolation: If one service fails (e.g., the Inventory service), the rest
of the application (e.g., Checkout service) can continue to function, perhaps with
reduced capability (degraded performance).
Real-Life Example (E-commerce Platform):
Instead of one massive Monolith where all logic (User, Inventory, Orders, Payments,
Shipping) is in a single codebase:
Technology Stack
Service Responsibility
Example
User Service User registration, login, profile [Link] Core API +
management. MongoDB
Product/Catalog Product details, search, inventory [Link] Core API + SQL
Service updates. Server
Order Service Creating and tracking orders, [Link] Core API +
checkout process. PostgreSQL
Payment Service Interfacing with third-party payment Java Spring Boot + Redis
gateways.
These services communicate with each other, typically over REST APIs.
REST (Representational State Transfer)
REST is an architectural style for distributed hypermedia systems. It is not a protocol or a
standard; it's a set of constraints that, when applied, result in a system with desirable
properties like performance, scalability, and modifiability.
How it Works (Key Constraints):
1. Client-Server: Separation of concerns. The client handles the user interface, and
the server handles data storage and logic.
2. Stateless: Each request from the client to the server must contain all the
information needed to understand the request. The server cannot rely on session
state stored on the server side.
3. Cacheable: Responses must explicitly or implicitly be defined as cacheable or
non-cacheable to prevent data inconsistency.
4. Uniform Interface (The most important): Defines how the client and server
communicate, involving:
o Resources: Everything is a resource (e.g., a User, an Order) identified by a
unique URI (e.g., /api/users/123).
o Standard Methods: Using standard HTTP methods to perform actions on
resources:
▪ GET: Retrieve a resource (safe, idempotent).
▪ POST: Create a new resource or perform a non-idempotent
operation.
▪ PUT: Fully replace a resource (idempotent).
▪ DELETE: Remove a resource (idempotent).
▪ PATCH: Partially update a resource.
o Self-Descriptive Messages: The request and response contain metadata
to describe the processing (e.g., HTTP status codes, MIME types).
Where to Apply:
• Web APIs (The primary use case): Creating the backbone for Single Page
Applications (SPAs), Mobile Apps, and integration with third-party systems.
• Microservices Communication: As mentioned, services often communicate
using RESTful APIs to share data and invoke actions.
• Public Data APIs: Exposing data to the public (e.g., a weather API, stock quotes
API).
Real-Life Example (Managing a Customer Resource):
HTTP
Action URI Description
Method
Get All GET /api/customers Retrieves a list of all customers.
Customers
Get a GET /api/customers/42 Retrieves details for customer with
Customer ID 42.
Create a POST /api/customers Sends new customer data in the
Customer request body.
Update a PUT /api/customers/42 Replaces customer 42's data
Customer entirely with the data in the
request body.
Delete a DELETE /api/customers/42 Removes customer 42 from the
Customer system.