Software Design Fundamentals 2024
If you’re wondering what software design actually is and why it matters, you’ve come to the right place. As an application developer, I want to walk you through the essentials.
What is Software Design?
Software design is the process of planning how your software should be structured. It’s not just about what the software should do, but how it should do it. Good design is the foundation of efficient, maintainable, and scalable software.
The three levels of software design:
- Architectural Design: The overall structure of your entire application
- Detailed Design: Concrete implementation of individual components
- Interface Design: Definition of APIs and user interfaces
Why Good Design Matters
Good design is critical to your software’s quality. It ensures that your software not only works today, but remains easy to extend and maintain in the future.
Benefits of good design:
- Maintainability: Easier to understand and modify
- Extensibility: New features can be added without major refactoring
- Testability: Components can be tested in isolation
- Reusability: Modules can be used in other projects
- Performance: Efficient use of resources
SOLID Principles
The SOLID principles are five foundational design principles for object-oriented software development:
1. Single Responsibility Principle (SRP)
A class should have only one reason to change.
// ❌ Bad: Class has multiple responsibilities
public class UserService {
public void saveUser(User user) { /* saves user */ }
public void sendEmail(User user) { /* sends email */ }
public void generateReport(User user) { /* generates report */ }
}
// ✅ Good: Each class has a single responsibility
public class UserService {
public void saveUser(User user) { /* saves user */ }
}
public class EmailService {
public void sendEmail(User user) { /* sends email */ }
}
public class ReportService {
public void generateReport(User user) { /* generates report */ }
}
2. Open/Closed Principle (OCP)
Software components should be open for extension but closed for modification.
// ❌ Bad: Must modify code for each new discount type
public class DiscountCalculator {
public double calculateDiscount(String type, double amount) {
if (type.equals("STUDENT")) return amount * 0.1;
if (type.equals("SENIOR")) return amount * 0.2;
// new type requires code change
return 0;
}
}
// ✅ Good: Open for extension
public interface Discount {
double calculate(double amount);
}
public class StudentDiscount implements Discount {
public double calculate(double amount) { return amount * 0.1; }
}
public class SeniorDiscount implements Discount {
public double calculate(double amount) { return amount * 0.2; }
}
3. Liskov Substitution Principle (LSP)
Subclasses should be substitutable for their base classes.
// ❌ Bad: Violates LSP
public class Rectangle {
protected int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Violates Rectangle behavior
}
}
// ✅ Good: Respects LSP
public abstract class Shape {
public abstract int getArea();
}
public class Rectangle extends Shape {
private int width, height;
public int getArea() { return width * height; }
}
public class Square extends Shape {
private int side;
public int getArea() { return side * side; }
}
4. Interface Segregation Principle (ISP)
Many client-specific interfaces are better than one general-purpose interface.
// ❌ Bad: Large interface
public interface Worker {
void work();
void eat();
void sleep();
}
public class Robot implements Worker {
public void work() { /* ... */ }
public void eat() { /* Robot doesn't eat */ }
public void sleep() { /* Robot doesn't sleep */ }
}
// ✅ Good: Specific interfaces
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class Robot implements Workable {
public void work() { /* ... */ }
}
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
// ❌ Bad: Direct dependency
public class LightSwitch {
private LightBulb bulb;
public LightSwitch() {
this.bulb = new LightBulb(); // Direct dependency
}
public void flip() {
bulb.turnOn();
}
}
// ✅ Good: Dependency on interface
public interface Switchable {
void turnOn();
void turnOff();
}
public class LightBulb implements Switchable {
public void turnOn() { /* ... */ }
public void turnOff() { /* ... */ }
}
public class LightSwitch {
private Switchable device;
public LightSwitch(Switchable device) {
this.device = device; // Dependency on abstraction
}
public void flip() {
device.turnOn();
}
}
Other Important Design Principles
KISS (Keep It Simple, Stupid)
Keep your code simple and understandable. Simple solutions are often the best ones.
DRY (Don’t Repeat Yourself)
Avoid code duplication. Don’t repeat logic across your codebase.
YAGNI (You Ain’t Gonna Need It)
Don’t build for hypothetical future scenarios. Focus on what’s needed right now.
Separation of Concerns
Break different aspects of your application into separate modules.
Architectural Patterns
Layered Architecture
Organizes code into logical layers:
- Presentation Layer: User interface
- Business Layer: Business logic
- Data Access Layer: Data access
- Database Layer: Database
MVC (Model-View-Controller)
Separates data, presentation, and control:
- Model: Data and business logic
- View: User interface
- Controller: Mediates between Model and View
Microservices Architecture
Splits the system into small, independent services:
- Each service has its own database
- Services communicate via APIs
- Independent deployment
Event-Driven Architecture
Systems communicate through events:
- Loose coupling between components
- Asynchronous communication
- Good scalability
Design Patterns (Gang of Four)
Creational Patterns
Factory Method
Creates objects without specifying their exact classes.
public interface Vehicle {
void drive();
}
public class Car implements Vehicle {
public void drive() { System.out.println("Car drives"); }
}
public class Motorcycle implements Vehicle {
public void drive() { System.out.println("Motorcycle drives"); }
}
public abstract class VehicleFactory {
public abstract Vehicle createVehicle();
}
public class CarFactory extends VehicleFactory {
public Vehicle createVehicle() { return new Car(); }
}
Singleton
Ensures a class has only one instance.
public class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() { /* private constructor */ }
public static DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
}
Structural Patterns
Adapter
Allows incompatible interfaces to work together.
public interface MediaPlayer {
void play(String audioType, String fileName);
}
public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
}
}
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
}
}
}
Behavioral Patterns
Observer
Enables notification when state changes occur.
import java.util.ArrayList;
import java.util.List;
public interface Observer {
void update(String message);
}
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
public void registerObserver(Observer observer) {
observers.add(observer);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(news);
}
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}
Strategy
Defines a family of algorithms and makes them interchangeable.
public interface PaymentStrategy {
void pay(int amount);
}
public class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card");
}
}
public class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
Refactoring Techniques
Extract Method
Pull code into a separate method to improve readability.
// Before
public void processOrder(Order order) {
// Validation
if (order == null) throw new IllegalArgumentException();
if (order.getItems().isEmpty()) throw new IllegalArgumentException();
// Calculation
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
// Storage
order.setTotal(total);
orderRepository.save(order);
}
// After
public void processOrder(Order order) {
validateOrder(order);
double total = calculateTotal(order);
saveOrder(order, total);
}
private void validateOrder(Order order) {
if (order == null) throw new IllegalArgumentException();
if (order.getItems().isEmpty()) throw new IllegalArgumentException();
}
private double calculateTotal(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
private void saveOrder(Order order, double total) {
order.setTotal(total);
orderRepository.save(order);
}
Replace Conditional with Polymorphism
Replace conditionals with polymorphic behavior.
// Before
public class Bird {
public void fly(String type) {
if (type.equals("Eagle")) {
System.out.println("Eagle flies high");
} else if (type.equals("Penguin")) {
System.out.println("Penguin cannot fly");
}
}
}
// After
public abstract class Bird {
public abstract void fly();
}
public class Eagle extends Bird {
public void fly() {
System.out.println("Eagle flies high");
}
}
public class Penguin extends Bird {
public void fly() {
System.out.println("Penguin cannot fly");
}
}
Code Quality Metrics
Cyclomatic Complexity
Measures code complexity by counting the number of decision points.
Maintainability Index
Evaluates how easily code can be maintained.
Test Coverage
The percentage of code covered by tests.
The Importance of Documentation
Good documentation is crucial for maintainability:
Types of Documentation
- API Documentation: Description of interfaces
- Architecture Documentation: Overview of system architecture
- Code Comments: Explanations of complex code sections
- User Documentation: Usage guides
Documentation Best Practices
- Document the “why”, not just the “what”
- Keep documentation up to date
- Use clear and understandable language
- Use diagrams for visualization
Practical Example: E-Commerce System
Here’s a practical example that applies many of the principles we’ve discussed:
// Interfaces for loose coupling
public interface OrderService {
Order createOrder(List<Item> items);
void processPayment(Order order, PaymentStrategy strategy);
}
public interface InventoryService {
boolean checkAvailability(Item item);
void reserveItem(Item item);
}
// Implementation with SOLID principles
@Service
public class OrderServiceImpl implements OrderService {
private final InventoryService inventoryService;
private final NotificationService notificationService;
public OrderServiceImpl(InventoryService inventoryService,
NotificationService notificationService) {
this.inventoryService = inventoryService;
this.notificationService = notificationService;
}
@Override
public Order createOrder(List<Item> items) {
validateItems(items);
reserveItems(items);
Order order = new Order(items);
order.calculateTotal();
return order;
}
@Override
public void processPayment(Order order, PaymentStrategy strategy) {
strategy.pay(order.getTotal());
notificationService.sendOrderConfirmation(order);
}
private void validateItems(List<Item> items) {
for (Item item : items) {
if (!inventoryService.checkAvailability(item)) {
throw new ItemNotAvailableException(item);
}
}
}
private void reserveItems(List<Item> items) {
for (Item item : items) {
inventoryService.reserveItem(item);
}
}
}
Exam Questions and Answers
1. What are the SOLID principles and why are they important?
Answer: The SOLID principles are five design principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. They are important because they lead to more maintainable, extensible, and testable code. They help avoid technical debt and improve overall code quality.
2. Explain the Single Responsibility Principle with an example.
Answer: The SRP states that a class should have only one reason to change. For example, a UserService class should be responsible only for user management, not email sending or report generation. This improves maintainability because changes to one responsibility don’t affect the other.
3. What is the difference between cohesion and coupling?
Answer: Cohesion describes how strongly the elements within a module belong together (high cohesion is good). Coupling describes how strongly modules depend on each other (low coupling is good). The goal is high cohesion and low coupling for better maintainability.
4. When do you use the Singleton pattern?
Answer: Singleton is used when exactly one instance of a class is needed, such as for database connections, logging services, or configuration managers. It ensures that globally only one instance exists and provides a central point of access.
5. Explain the Open/Closed Principle.
Answer: The OCP states that software components should be open for extension but closed for modification. This means you should be able to add new functionality through extension (for example, inheritance) without having to change existing code.
Recommended Books on Software Design
These are external affiliate links. We both benefit when you purchase through them:



