Skip to main content

Software Design and Architecture

Learning Objectives

By the end of this page, you will be able to:

  • Define software architecture and distinguish it from low-level software design.
  • Compare monolithic, microservices, and event-driven architectural styles, including trade-offs of each.
  • Explain the four pillars of OOP (encapsulation, abstraction, inheritance, polymorphism) with concrete examples.
  • Describe at least three common design patterns (Singleton, Factory Method, Observer) and the problem each solves.
  • Apply SOLID principles to evaluate whether a piece of design is well-structured.
  • Choose an appropriate architectural style for a given project scenario and justify the choice.

Quick Answer

Software design and architecture is the discipline of deciding how a software system is structured before (and while) it's built — what components exist, how they communicate, and how responsibilities are divided. Architecture operates at the system level (should this be one deployable unit or many independent services?), while design operates at a finer grain (how should this specific module or class be organized?). Getting this right matters because architecture is the hardest thing to change after the fact — a wrong database choice can be swapped in a sprint, but a wrong architecture (say, a monolith that needed to be microservices from day one) can take months to unwind. Good architecture and design make software easier to extend, test, and scale; poor choices compound into technical debt that slows every future change.

Core Content

Why Architecture Is Different From "Just Writing Code"

A common beginner assumption is that architecture is something senior engineers draw on whiteboards and everyone else just writes functions. In reality, every piece of code someone writes either reinforces or undermines the intended architecture. A single tightly-coupled shortcut — one module reaching directly into another's internal data instead of going through its interface — can quietly erode a carefully planned microservices boundary. Architecture is a set of decisions and constraints that must be actively maintained, not a diagram that gets drawn once and forgotten.

Definition: Software architecture is the high-level structure of a software system — its major components, their responsibilities, and how they interact — while software design refines that structure into concrete classes, modules, and algorithms.

System Architecture

Definition: System architecture is the overall organization of a software system's components — how it's divided into services or modules, and how those pieces communicate.

Explanation: Architecture decisions answer questions like: should this be one deployable application or many independently deployable services? Should components communicate through direct function calls, HTTP APIs, or asynchronous messages? Should there be a shared database or one database per service? These decisions are made early because they're expensive to reverse — unlike a bug in a function, an architectural mismatch (e.g., needing independent scaling for a component that's welded into a monolith) usually requires significant restructuring to fix.

Example: A to-do list app might use a simple three-layer architecture: a UI layer, a business logic layer, and a data layer, all deployed as a single application talking to one database.

Real-World Example: Netflix's architecture famously grew from a monolith into hundreds of microservices as its scale demanded independent scaling of components like recommendations, streaming, and billing — each now owned by a separate team, deployed separately, and scaled based on its own load pattern.

Why It Matters: The chosen architecture sets a ceiling on how easily the system can scale, how independently teams can work, and how quickly a bug in one area can be isolated from the rest of the system.

Common Misunderstanding: Students often think "bigger" architecture (microservices) is always "better" architecture. In reality, microservices introduce real costs — network latency, distributed debugging, operational complexity — that are wasted overhead for a small team or a low-traffic product.

Monolithic vs. Microservices vs. Event-Driven Architecture

Definition: Three common architectural styles that differ in how they divide responsibility and how components communicate: monolithic (a single deployable unit), microservices (many independently deployable services), and event-driven (components communicate by producing and reacting to events).

Explanation: A monolithic architecture keeps all functionality in one codebase and one deployment — simple to build, test, and deploy, but harder to scale specific parts independently as the codebase grows. Microservices split functionality into small, independently deployable services that communicate over the network, allowing each part to scale, deploy, and even use a different tech stack independently — at the cost of operational complexity and network overhead. Event-driven architecture organizes communication around events (something happened) rather than direct requests, which decouples producers and consumers of information and suits real-time or asynchronous workloads.

Example: A student project to-do app: monolithic works fine — one small team, one deployable, low traffic. A ride-sharing platform: microservices make sense — ride matching, payments, and driver management scale very differently and are owned by different teams.

Real-World Example: Uber runs ride matching, payment processing, and driver management as separate microservices, communicating in part through event-driven mechanisms so that, for example, a "ride completed" event can simultaneously trigger payment, trigger a rating request, and update driver availability — without those three concerns needing to know about each other directly.

Why It Matters: Choosing the wrong style either wastes engineering effort managing complexity that isn't needed yet (premature microservices) or forces a costly rewrite when a monolith can no longer scale or be maintained by a growing team.

Common Misunderstanding: Students sometimes think microservices and event-driven architecture are the same thing. Microservices is about how functionality is divided; event-driven is about how components communicate. A microservices system can communicate via direct API calls (not event-driven), and a monolith can internally use an event-driven pattern (e.g., an internal event bus) without being split into services.

Software Design Patterns

Definition: Design patterns are named, reusable solutions to recurring problems in software design, capturing proven structures rather than reinventing them each time.

Explanation: Patterns give developers a shared vocabulary — saying "use a Factory Method here" communicates an entire structural approach instantly to another engineer familiar with the pattern. Common patterns include Singleton (ensure a class has exactly one instance, e.g., a single configuration manager), Factory Method (delegate object creation to a subclass or method instead of hardcoding a specific class), Observer (let objects subscribe to and be notified of changes in another object, the basis of most event-handling systems), Strategy (make an algorithm swappable at runtime, e.g., choosing between different sorting or pricing strategies), and Command (encapsulate a request as an object, enabling features like undo/redo or task queues).

Example: An app with light/dark themes uses the Strategy pattern: a ThemeStrategy interface with LightTheme and DarkTheme implementations lets the app switch appearance without changing the rendering code.

Real-World Example: GUI frameworks like Java Swing or web frameworks' event listeners are built on the Observer pattern — a button "observes" clicks and notifies all registered listeners, decoupling the button itself from whatever logic responds to being clicked.

Why It Matters: Patterns aren't decoration — they solve real coupling and flexibility problems. Using the wrong pattern (or none at all) where one clearly applies tends to produce code that's hard to extend without modifying existing, tested logic.

Common Misunderstanding: Beginners sometimes force patterns into code that doesn't need them ("pattern for pattern's sake"), adding indirection and complexity without solving an actual problem. A pattern should be reached for because a specific recurring problem exists, not because it looks sophisticated.

Principles of Object-Oriented Programming

Definition: The four foundational ideas of OOP — encapsulation, abstraction, inheritance, and polymorphism — that guide how classes and objects are structured to be maintainable and reusable.

Explanation: Encapsulation bundles data and the methods that operate on it together, hiding internal state behind a controlled interface so external code can't corrupt it directly. Abstraction exposes only what's necessary and hides implementation detail, letting users of a class think in terms of what it does rather than how. Inheritance lets a class reuse and extend behavior from a parent class, avoiding duplicated logic across related types. Polymorphism lets different classes be treated through a common interface, so the same method call produces type-appropriate behavior.

Example: A Shape class hierarchy: Circle and Rectangle both inherit from Shape and implement area() differently (polymorphism), while each hides its own internal fields like radius or width/height behind that shared interface (encapsulation and abstraction).

Real-World Example: In a payment system, a PaymentMethod interface implemented by CreditCard, PayPal, and BankTransfer classes lets the checkout code call process(amount) on any of them without knowing which one it's dealing with — new payment methods can be added later without touching the checkout logic at all.

Why It Matters: These principles are what make large codebases survivable — without them, adding a feature often means hunting down and modifying scattered, duplicated logic across the entire system.

Common Misunderstanding: Inheritance is often overused by beginners to share code, even when the relationship isn't truly "is-a" (e.g., making Square inherit from Rectangle breaks down under certain operations). Composition ("has-a," combining smaller objects) is frequently the safer, more flexible choice than inheritance.

SOLID and Design Best Practices

Definition: SOLID is a set of five design principles — Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion — that guide maintainable object-oriented design.

Explanation: Single Responsibility says a class should have exactly one reason to change. Open-Closed says code should be open to extension but closed to modification — you should be able to add behavior without editing existing, tested code. Liskov Substitution says a subclass must be usable anywhere its parent class is expected without breaking correctness. Interface Segregation says clients shouldn't be forced to depend on methods they don't use. Dependency Inversion says high-level modules shouldn't depend on low-level implementation details directly — both should depend on abstractions.

Example: Violating Single Responsibility: a User class that handles authentication, sends emails, and generates PDF reports. Fixing it means splitting those into Authenticator, EmailService, and ReportGenerator classes, each with one reason to change.

Real-World Example: Dependency Inversion is why most backend frameworks let you swap a database (e.g., PostgreSQL for MySQL) by changing a configuration and an adapter, rather than rewriting business logic — the business logic depends on a database interface, not a specific database implementation.

Why It Matters: Code that follows SOLID tends to be easier to test (each piece can be tested in isolation) and easier to extend (new features rarely require touching stable, already-tested code).

Common Misunderstanding: SOLID is sometimes treated as a rigid checklist to apply everywhere. In small, simple programs, strictly applying all five principles can add unnecessary abstraction; SOLID pays off most as complexity and team size grow.

Layered View of Design Decisions

Key Terms

TermDefinitionContext/Related
Software ArchitectureThe high-level structure of a system: its major components and how they interactSet early, expensive to change
Monolithic ArchitectureA single deployable unit containing all application functionalitySimple to build; harder to scale independently
Microservices ArchitectureAn architecture splitting functionality into independently deployable servicesScales well; adds operational complexity
Event-Driven ArchitectureAn architecture where components communicate by producing/consuming eventsSuits real-time, asynchronous systems
Design PatternA named, reusable solution to a recurring software design problemE.g., Singleton, Factory Method, Observer
Singleton PatternEnsures a class has exactly one instance with a global access pointCommon for shared config/resource managers
Observer PatternLets objects subscribe to and be notified of another object's state changesBasis of GUI event handling
EncapsulationBundling data and methods together while hiding internal stateOOP pillar
AbstractionExposing only necessary detail while hiding implementationOOP pillar
InheritanceA class reusing and extending behavior from a parent classOOP pillar; can be overused
PolymorphismTreating different classes through a common interface, each behaving appropriatelyOOP pillar
SOLIDFive principles (Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) for maintainable OOP designPays off most in larger, evolving codebases
Technical DebtThe future cost incurred by choosing a quicker, lower-quality design nowAccumulates from rushed architecture/design decisions

Common Mistakes

  1. Misconception: Microservices architecture is always a better, more "modern" choice than a monolith. Why It's Wrong: Microservices add real operational cost — network calls replace function calls, debugging spans multiple services, and deployment coordination becomes harder — costs that outweigh the benefit for small teams or low-traffic systems. Correct Understanding: The right architecture depends on scale, team size, and how independently different parts of the system need to evolve — a monolith is often the correct starting choice, with the option to extract services later as real scaling needs emerge.

  2. Misconception: Design patterns should be used as often as possible to write "proper" object-oriented code. Why It's Wrong: Applying a pattern where no corresponding problem exists adds indirection and complexity without benefit, making the code harder to read for no gain. Correct Understanding: A pattern should be introduced only when its specific problem (e.g., needing swappable algorithms, or notifying multiple listeners of a change) is actually present in the design.

  3. Misconception: Inheritance is the primary tool for reusing code between classes. Why It's Wrong: Inheritance creates a rigid "is-a" relationship that can break unexpectedly (e.g., a Square that inherits from Rectangle violates expected behavior if width and height are set independently), and changes to a parent class ripple through every subclass. Correct Understanding: Composition ("has-a" relationships, combining smaller objects) is often more flexible than inheritance and should be preferred unless a true "is-a" relationship exists and is expected to remain stable.

Comparison and Connections

Style/ConceptBest ForScalabilityComplexityDeployment
MonolithicSmall teams, early-stage productsVertical scaling onlyLowSingle deployable unit
MicroservicesLarge, complex products needing independent scalingScales each service independentlyHigh (network, coordination)Many independent deployments
Event-DrivenReal-time, asynchronous, loosely coupled systemsScales well for asynchronous loadMedium-High (debugging async flows)Varies; often paired with microservices
InheritanceReusing behavior in a true "is-a" hierarchyN/A (design-level)Can create rigid couplingN/A
CompositionFlexible reuse without rigid hierarchyN/A (design-level)Lower coupling than inheritanceN/A

Practice Questions

Recall

  1. What are the four pillars of Object-Oriented Programming? Answer: Encapsulation, Abstraction, Inheritance, and Polymorphism.

  2. Name the five SOLID principles. Answer: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.

Understanding

  1. Why is architecture considered harder to change than low-level design decisions? Answer: Architecture decisions (e.g., monolith vs. microservices, database-per-service vs. shared database) shape how the entire system is deployed, scaled, and organized around teams — reversing them means restructuring large portions of the codebase and infrastructure, unlike swapping a single class's implementation.

  2. Why can overusing inheritance make a codebase harder to maintain? Answer: Inheritance tightly couples subclasses to their parent's implementation, so a change to the parent can unexpectedly break subclasses, and forcing a non-"is-a" relationship into an inheritance hierarchy (e.g., Square inheriting Rectangle) can violate expected behavior.

Application

  1. A small team of three developers is building an MVP for a new app and expects fewer than 1,000 users in the first year. Which architectural style should they choose, and why? Answer: Monolithic — with a small team and low expected load, a monolith is faster to build, simpler to deploy and debug, and avoids the operational overhead of microservices that wouldn't yet be justified by scale.

  2. A checkout system needs to support credit card, PayPal, and bank transfer payments, with more payment methods expected later. Which design pattern fits, and how would you apply it? Answer: The Strategy pattern (or a similar polymorphic interface approach) — define a common PaymentMethod interface with a process(amount) method, implement it for each payment type, and let the checkout code call process() without knowing which concrete payment method it's using, so new methods can be added without modifying checkout logic.

Analysis

  1. Compare how the Open-Closed Principle and the Observer pattern both address the same underlying goal of avoiding modification to existing, tested code. Answer: The Open-Closed Principle is the general design goal — code should be extendable without being modified. The Observer pattern is one concrete mechanism for achieving it: instead of modifying a subject's code every time a new type of reaction is needed, new observers can simply subscribe to existing events, extending behavior without touching the subject's implementation.

  2. A startup builds a monolith, succeeds, and two years later struggles to scale because one feature (image processing) needs far more compute than the rest of the app. Analyze what architectural change would help and why it wasn't necessary earlier. Answer: Extracting image processing into its own microservice would let it scale independently (e.g., on GPU-optimized servers) without over-provisioning the entire monolith. It wasn't necessary earlier because, at low scale, the mismatch in resource needs across features was negligible — the cost of splitting into services would have outweighed the benefit until real, uneven load appeared.

FAQ

Q: Is software architecture the same as software design? A: They're related but operate at different altitudes. Architecture is the high-level structure — how the system is divided into major components and how those communicate. Design refines that structure into concrete classes, modules, and algorithms within each component.

Q: When should a team choose microservices over a monolith? A: When different parts of the system have genuinely different scaling, deployment, or team-ownership needs — for example, when one feature needs to scale 100x more than the rest, or when multiple independent teams need to deploy without coordinating releases.

Q: Are design patterns language-specific? A: No — patterns like Singleton, Observer, and Strategy are language-independent ideas about structuring relationships between objects. The implementation syntax differs across languages (e.g., Python, Java, C++), but the underlying problem and solution shape are the same.

Q: Why do experienced engineers prefer composition over inheritance? A: Composition builds behavior by combining smaller, independent objects ("has-a"), which is more flexible and less fragile than inheritance ("is-a"), because changes to one composed object don't ripple through a rigid class hierarchy the way changes to a parent class do.

Q: How does event-driven architecture relate to microservices? A: They're often combined but are independent concepts — microservices describes how functionality is divided into services, while event-driven describes how those services (or components within a monolith) communicate. Many microservices systems use events to reduce direct dependencies between services.

Quick Revision

  • Architecture = high-level system structure; design = finer-grained class/module structure within it.
  • Monolithic: single deployable unit, simple, harder to scale independently.
  • Microservices: independently deployable services, scales well, adds operational complexity.
  • Event-driven: components communicate via events, decoupling producers and consumers.
  • Design patterns are reusable solutions to recurring problems — Singleton, Factory Method, Observer, Strategy, Command.
  • OOP pillars: Encapsulation, Abstraction, Inheritance, Polymorphism.
  • SOLID principles guide maintainable OOP design: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
  • Prefer composition over inheritance unless a true, stable "is-a" relationship exists.
  • Architecture decisions are expensive to reverse — choose based on actual scale/team needs, not hype.
  • Patterns should solve a real, present problem — not be added for their own sake.
  • Technical debt accumulates when design shortcuts are taken under time pressure.
  • Real systems (Uber, Netflix) mix architectural styles — microservices with event-driven communication is common at scale.

Prerequisites

  • Software Development Life Cycle
  • Requirements Analysis and Specification

Related Topics

  • Software Testing and Quality Assurance
  • Agile and DevOps Methodologies

Next Topics

  • Software Testing and Quality Assurance
  • Software Project Management