design patterns

Gergő Pintér, PhD

gergo.pinter@uni-corvinus.hu

software design and architecture stack

based on Khalil Stemmel’s figure [1]

structural pattern matching

a paradigm principle example in functional programming, in addition to OOP’s from week 4

foo = 3

match foo:
    case 1:
        print("it's one")
    case 2:
        print("it's two")
    case _:
        print("it's not one or two")

structural pattern matching

a paradigm principle example in functional programming, in addition to OOP’s from week 4

foo = [1, 2.4, 3.1]

match foo:
    case [1, 2, 3]:
        print("it's a list with 1, 2, and 3")
    case [1, 2, _]:
        print("it's a list with 1, 2, and whatever")
    case [1, float(), float()]:
        print("it's a list with 1, and two floats")

gang of four (GoF) design patterns

  • GoF: Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
  • 23 common software design patterns
    • published in “Design Patterns: Elements of Reusable Object-Oriented Software” (1994) [2]
  • provides solutions to common design problems
  • categorized into three main groups
    1. creational
    2. structural
    3. behavioral

the 23 (GoF) design patterns

creational

  • Factory Method
  • Abstract Factory
  • Builder
  • Prototype
  • Singleton

structural

  • Adapter
  • Bridge
  • Composite
  • Decorator
  • Facade
  • Flyweight
  • Proxy

behavioral

  • Chain of Responsibility
  • Command
  • Interpreter
  • Iterator
  • Mediator
  • Memento
  • Observer
  • State
  • Strategy
  • Template Method
  • Visitor

read about the design patterns in details, for example at refactoring.guru

bridge pattern (structural)

The bridge pattern lets you split a large class or a set of closely related classes into two separate hierarchies.

observer pattern (behavioral)

  • instead of flooding the service with update requests, or the server sending notifications to everyone clients can subscribe to notifications
  • this is exactly how newsletters work

observer pattern - Python example

class Observable:
    def __init__(self):
        self.subscribers = []

    def subscribe(self, subscriber):
        self.subscribers.append(subscriber)

    def notify(self, data):
        for subscriber in self.subscribers:
            subscriber.update(data)

class Subscriber:  # aka Observer
    def __init__(self, name):
        self.name = name

    def update(self, data):
        print(f"{self.name} received data: {data}")
class TemperatureSensor(Observable):
    def __init__(self):
        super().__init__()

    def change_temperature(self, temp):
        print(f"Temperature changed to: {temp}")
        self.notify(temp)

class DisplayUnit(Subscriber):
    def update(self, temp):
        print(f"{self.name} updated with temperature: {temp}")
sensor = TemperatureSensor()  # create observable

display1 = DisplayUnit("Display 1")  # create observer
display2 = DisplayUnit("Display 2")  # create observer

sensor.subscribe(display1)
sensor.subscribe(display2)

sensor.change_temperature(25)  # change value
Temperature changed to: 25
Display 1 updated with temperature: 25
Display 2 updated with temperature: 25

GoF design patterns in functional programming

OO pattern FP pattern
factory pattern function
strategy pattern function
decorator pattern function
visitor pattern function

Peter Norvig demonstrated that 16 out of the 23 patterns are simplified or eliminated by language features in Lisp or Dylan (1998) [3]

more about it from Scott Wlaschin [4]

map-filter-reduce

some FP design patterns

map

apply a function to each item and produce a new collection

Enum.map([0, 1, 2, 3], fn(x) -> x - 1 end)
[-1, 0, 1, 2]

filter

filter the collection to include only those elements that evaluate to true using the provided function

Enum.filter([1, 2, 3, 4], fn(x) -> rem(x, 2) == 0 end)
[2, 4]

remove odd elements

reduce

distill the collection down into a single value

Enum.reduce([1, 2, 3], fn(x, acc) -> x + acc end)
6

examples and definitions are from Elixir School by Sean Callan

You aren’t gonna need it (YAGNI)

  • states that a programmer should not add functionality until deemed necessary
  • principle originates from extreme programming (XP)

Always implement things when you actually need them, never when you just foresee that you need them.

Ron Jeffries

extreme programming

  • advocates frequent releases in short development cycles
  • intended to improve productivity and introduce checkpoints at which new customer requirements can be adopted
  • features
    • programming in pairs,
    • doing extensive code review,
    • unit testing of all code,
    • not programming features until they are actually needed,
    • flat management structure
  • considered a type of agile software development

SOLID principles

SOLID is a mnemonic acronym for five design principles intended to make object-oriented designs more understandable, flexible, and maintainable [5]

  • single responsibility principle
  • open-closed principle
  • Liskov substitution principle
  • interface segregation principle
  • dependency inversion principle

single responsibility principle

a class should do one thing and therefore it should have only a single reason to change

Unix philosophy

Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new “features”.

advantages

  • testing is easier
    • fewer test cases required
  • less dependencies
    • to other modules or classes

open-closed principle

classes should be open for extension and closed to modification

class Shape:
    pass


class Square(Shape):
    def __init__(self, width: float):
        self.width = width

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

class AreaCalculator:

    def sum(self, shapes: list[Shape]) -> float:
        result = 0
        for shape in shapes:
            if isinstance(shape, Square):
                result += shape.width**2
            elif isinstance(shape, Circle):
                result += shape.radius**2 * math.pi

        return round(result, 2)

example based on [6]

open-closed principle

class Shape:
    pass

class AreaInterface:
    def area(shape: Shape) -> float:
        pass

class Square(Shape, AreaInterface):
    def __init__(self, width: float):
        self.width = width

    def area(self) -> float:
        return self.width**2

class Circle(Shape, AreaInterface):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return round(self.radius**2 * math.pi, 2)

class AreaCalculator:
    def sum(self, shapes: list[Shape]) -> float:
        return sum([i.area() for i in shapes])

example based on [6]

Liskov substitution principle

if class A is a subtype of class B, B should be able to replaced with A without disrupting the behavior of the program [7]

  • named after Barbara Liskov
  • presented first in 1987 [8]
  • circle-ellipse problem / square–rectangle problem
    • existence of the circle–ellipse problem is used to criticize object-oriented programming [9]

Liskov substitution principle - example

class Rectangle:

    def __init__(self, width: int, height: int):
        self.__width = width
        self.__height = height

    def setWidth(self, width: int):
        self.__width = width

    def setHeight(self, height: int):
        self.__height = height

    def getWidth(self):
        return self.__width

    def getHeight(self):
        return self.__height

    def getArea(self):
        return self.__width * self.__height
class Square(Rectangle):

    def __init__(self, width: int):
        super().setWidth(width)
        super().setHeight(width)

    def setWidth(self, width: int):
        super().setWidth(width)
        super().setHeight(width)

    def setHeight(self, height: int):
        super().setWidth(height)
        super().setHeight(height)
>>> r = Rectangle(2, 3)
>>> print(r.getArea())
6

>>> s = Square(2)
>>> print(s.getArea())
4

code is based on [10]

Liskov substitution principle - example

def getAreaTest(r: Rectangle):
    width = r.getWidth()  # width is 2
    r.setHeight(10)
    return f"Expected area of {width * 10}, got {r.getArea()}"
>>> r = Rectangle(2, 3)
>>> print(r.getArea())
6

>>> s = Square(2)
>>> print(s.getArea())
4

>>> print(getAreaTest(r))  # rectangle
Expected area of 20, got 20

>>> print(getAreaTest(s))  # square
Expected area of 20, got 100

this example violates the Liskov substitution principle

code is based on [10]

interface segregation principle

states that many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do no need.

example based on [6]

dependency inversion principle

Dependency inversion principle says that modules should depend upon interfaces or abstract classes, not concrete classes. It’s an inversion because implementations depend upon abstractions and not the other way round. [7]

increases reusability

hollywood principle (inversion of control)

don’t call us, we’ll call you

  • for control flow management
  • IoC shifts control from the application to an outside framework
  • promotes a more modular design by decoupling components
    • however, adding an IoC framework can increase complexity
    • with a significant learning curve for those unfamiliar with the concept
  • observer pattern (Gang of Four design pattern) is a form of IoC

coupling

  • the degree of interdependence between software modules
  • coupling is usually contrasted with cohesion
    • low coupling often correlates with high cohesion, and vice versa
from Wikimedia | public domain

source Wikipedia [12]

law of unintended consequences

In the social sciences, unintended consequences (more colloquially called knock-on effects) are outcomes of a purposeful action that are not intended or foreseen. [13]

  • in software engineering, this could be introducing bugs or performance issues when fixing something or adding new features
  • high coupling at class/module level or side effects at function level can contribute to this

law of Demeter

An object should only interact with its immediate friends, not strangers.

– Ian Holland et al. (1987) [14]

  • an object should only call methods of:
    • itself,
    • its direct components,
    • its function parameters,
    • or objects it creates
  • it should not reach an object through another one
based on [14]

source: Laws of Software Engineering / Law of Demeter by Dr. Milan Milanović

law of Demeter

  • if object A only calls its immediate friend (B) and doesn’t reach into its internals (like C), then changes to C or removal of C don’t affect A
    • this way, each class knows as little as possible about others, reducing the impact of changes
    • decreases dependency & results looser coupling
  • this often leads to adding wrapper methods
    • while that might increase the number of methods, it results in cleaner interactions
based on [14]

source: Laws of Software Engineering / Law of Demeter by Dr. Milan Milanović

layers of an architecture

  • presentation layer (frontend/UI)
    • handles the interactions that users have with the software
    • focuses on the user interface and user experience – later in course
  • business logic layer (domain layer)
  • application layer (service layer)
  • data access layer (persistence layer)
  • data layer

source: Layers in software architecture by Sagar Hudge [15]

layers of an architecture

  • presentation layer (frontend/UI)
  • business logic layer (domain layer)
    • where business rules and application logic are implemented
    • processes data, applies business rules, and controls transactions
  • application layer (service layer)
  • data access layer (persistence layer)
  • data layer

source: Layers in software architecture by Sagar Hudge [15]

layers of an architecture

  • presentation layer (frontend/UI)
  • business logic layer (domain layer)
  • application layer (service layer)
    • acts as a bridge between the presentation layer and business logic
    • manages application flow, coordinates user requests, and processes business operations
  • data access layer (persistence layer)
  • data layer

source: Layers in software architecture by Sagar Hudge [15]

layers of an architecture

  • presentation layer (frontend/UI)
  • business logic layer (domain layer)
  • application layer (service layer)
  • data access layer (persistence layer)
    • handles data storage and retrieval from persistent storage systems (e.g., database)
    • manages CRUD operations and interacts with data sources
  • data layer

source: Layers in software architecture by Sagar Hudge [15]

layers of an architecture

  • presentation layer (frontend/UI)
  • business logic layer (domain layer)
  • application layer (service layer)
  • data access layer (persistence layer)
  • data layer
    • where data is stored in a structured or semi-structured format

source: Layers in software architecture by Sagar Hudge [15]

how separating layers helps to design an architecture?

  • separation of concerns: each element has a clear responsibility, making the codebase easier to understand, maintain, and modify
  • modularity and reusability: elements (components or services) that can be used across different parts of the application or even other projects
  • scalability: changes in one element (e.g., database layer) do not necessarily affect others, easier to replace one as the software scales
  • ease of testing: each element can be tested individually

source: Layers in software architecture by Sagar Hudge [15]

topologies

Object-oriented design (OOD) is the process of planning a system of interacting objects to solve a software problem [16].

control flow? structure?

historically grown architecture based on [17]

based on Cth027’s figure | CC BY-SA

server/client architecture

  • consists of two parts
    • client and server
  • distributed
  • always the client initiates a connection to the server
  • while the server process always waits for requests from any client

message bus

  • shared communication channel that connects multiple components or services
  • simple, extensible

CAN bus

message bus types

models

  • publish-subscribe model
    • messages are published to a specific topic, and all subscribed receivers receive those messages
    • one to many
  • point-to-point model
    • messages are sent directly from a sender to a specific receiver, ensuring that only that recipient processes the message
    • one to one

delivery guaranties

  • at most once
    • push based
    • no retries
  • at least once
    • delivery confirmation
    • (typically) pull based
  • exactly once
    • at least once, extended by guarantee that there will be no duplicates

based on [18] and [19]

layered

number of layers in a layered architecture is not set to a specific number

  • presentation layer (a.k.a. UI layer, view layer)
    • responsible for user interactions with the software system
  • application layer (a.k.a. service layer)
    • aspects related to accomplishing functional requirements
  • business (logic) layer
    • responsible for algorithms, and programming components
  • data access layer (a.k.a. persistence layer)
    • responsible for handling data, databases

layered - properties

advantages

  • simple and easy to learn and implement
  • reduced dependency because the function of each layer is separate from the other layers
  • testing is easier because of the separated components
    • components can be tested individually
  • cost overheads are fairly low

disadvantages

  • scalability is difficult
    • not well-suited for large projects
  • can be difficult to maintain
    • a change in a single layer can affect the entire system because it operates as a single unit
  • a layer depends on the layer above it

based on [20]

onion architecture

  • popularized by Jeffrey Palermo
  • code can depend on layers more central, but code cannot depend on layers further out from the core
    • all coupling is toward the center
  • the database is not the center, it is external
    • the data model is in focus, whereas in layered data is the foundation
  • relies on the dependency inversion principle
  • appropriate for long-lived business applications
    • also applications with complex behavior

based on [21]

hexagonal - motivation

  • invented by Alistair Cockburn [22]
  • application should be equally controllable by users, other applications, or automated tests
    • for the business logic, it makes no difference whether it is invoked from a user interface, a REST API, or a test framework
  • infrastructure modernization should be possible without changing the business logic

based on [17]

hexagonal (ports & adapters)

advantages
  • modifiability
  • isolates responsibilities
  • once the ports are defined, the work on the components can be divided among developers
disadvantages
  • the effort of port-adapter implementation is non-negligible
  • for smaller applications, the extra effort is not worth it

  • hexagonal architecture does not specify what is inside the application hexagon
  • represents a single design decision:
    • wrap your application in an API and put tests around it

based on [17]

hexagonal vs. layered

can be extend without changing the business logic

it is very similar to the onion and (the clean architecture [23])

based on [17]

the clean architecture

  • by Robert C. Martin [23], unifies onion or hexagonal, etc.
  • source code dependencies can only point inwards
  • relies on dependency inversion principle
  • the number of circles is flexible

Model-View-Controller [24]

  • architectural pattern
  • MVC pattern was implemented as early as 1974 in the Smalltalk project
  • view is responsible for rendering UI
  • controller responds to the user input and performs interactions on the data model
  • model is responsible for managing the data
  • the view and the model are tightly coupled
  • view is monolithic and usually couples tightly with the UI framework
    • unit testing the view becomes difficult

MVC - MVP - MVVM

ASP.NET, Django (Python), Ruby on Rails, Laravel (PHP)

Windows Forms, Java Swing

WPF, AngularJS

figures based on [25]

other alternatives: Alternatives To MVC - by Anthony Ferrara

common pitfalls of architecture design and how to avoid them

tight coupling between layers

~ - use techniques like inversion of control for flexible dependency management
  • each layer should only interacts with its adjacent layer through well-defined interfaces
too many layers
  • define the essential layers based on your application requirements; 4‑5 layers are usually enough
  • avoid splitting responsibilities into too many small layers without reason

source: Layers in software architecture by Sagar Hudge [15]

common pitfalls of architecture design and how to avoid them

cross-layer communication
  • violates the separation of concerns
  • each layer only interacts with its adjacent layers (see law of Demeter)
mixing business logic in the presentation layer
  • happens when business rules implemented directly in the presentation layer resulting duplicated or inconsistent logic, which is harder to update, scale, or test
  • keep business logic in the business logic layer and presentation layer should only display data/status updates

source: Layers in software architecture by Sagar Hudge [15]

common pitfalls of architecture design and how to avoid them

failure to refactor and maintain the architecture
  • “over time, the architecture may become outdated or cluttered with technical debt, making it hard to scale or add new features” [15]
  • regularly review and refactor the codebase to keep layers clean and maintainable – iterative design, boys scout rule
lack of proper layer abstraction
  • “if layers are not abstracted properly (e.g., exposing internal implementation details), it becomes difficult to replace or update components without affecting other layers” [15]
  • each layer should have a clear interface that hides internal details from other layers

source: Layers in software architecture by Sagar Hudge [15]

law of leaky abstractions

All non-trivial abstractions, to some degree, are leaky.

– Joel Spolsky

  • refers to a design flaw where an abstraction, intended to simplify and hide the underlying complexity of a system, fails to completely do so
  • this results in some of the implementation details becoming exposed or ‘leaking’ through the abstraction,
  • forcing users to have knowledge of these underlying complexities to effectively use or troubleshoot the system

source: Wikipedia / Leaky abstraction [26]

user statistics example

as a user I want to see my activity to see my progress

display user statistics including
  • username
  • profile image
  • registration date
  • progress in course
  • daily activity in the current month

architecture v1

send everything to the UI

architecture v1 - class

in this case the UI has to calculate the daily activity

  • tight coupling
  • single responsibility principle violated

architecture v2

send only the aggregated data

architecture v2 - class

data collector still has the whole user data but that aligns with its purpose

data aggregator calculates everything and the UI only displays it

architecture v2.1 - class

UI might be on a client

different code base, different language

architecture v3

make the database aggregate the data

architecture v3 - SQL

for the activity matrix:

SELECT
    CAST(strftime('%W', timestamp) AS INTEGER) AS week_of_year,
    CAST(strftime('%u', timestamp) AS INTEGER) AS day_of_week,
    count(*) AS count
FROM activity
WHERE
    user_id = 42 AND
    week_of_year > 35 AND
    week_of_year < 40
GROUP BY
    week_of_year,
    day_of_week
;

architecture v3 - SQL

for the progress:

SELECT
    lesson / 50.0 AS progress
FROM activity
WHERE
    user_id = 42 AND
    result = 'success'
ORDER BY
    lesson DESC
LIMIT 1;

architecture v3 - issues

  • hard dependency on database
    • business logic in persistence layer
    • code depends on the SQL dialect
      • can be mitigated with an object-relational mapping (ORM) framework but that would also be a dependency
  • may not suitable for complex aggregations
    • stored functions just increase dependency
  • harder to unit test

on the other hand, most of these are present in all the three architectures!

record architecture decisions

Architecture represents the significant design decisions that shape a system, where significant is measured by cost of change. – Grady Booch

Developers working on that project have a shared understanding of the system design. […] This understanding includes how the system is divided into components and how the components interact through interfaces. – Ralph Johnson

# Title

## Status

What is the status, such as proposed, accepted, rejected, deprecated, superseded, etc.?

## Context

What is the issue that we're seeing that is motivating this decision or change?

## Decision

What is the change that we're proposing and/or doing?

## Consequences

What becomes easier or more difficult to do because of this change?

ADR template by Michael Nygard from Documenting architecture decisions, where each architecture decision record have these sections.

why write ARDs?

  • they’re not for you, they’re for the future you
    • ADRs capture the decision at the time it’s being made
      • on a meeting, on Slack, Teams, Zoom, etc.
      • like a structured memo
  • they’re not for you, they’re for your peers
    • ADRs help your teammates understand why the feature is built the way it is and not built some other way
      • alternatives considered and pros/cons within the ADRs
  • they’re not for you, they’re for your future peers
    • writing down decisions help communicate to your current teammates, but also those who will join later
    • it is an asynchronous way of communication, no need for a Zoom call, which reduces interruption

based on Why Write ADRs by Eli Perkins

summary

architectural patterns model-view-controller, MVP, MVVM
architectural styles layered, onion, hexagonal, clear, client-server
architectural principles coupling & cohesion
design patterns GOF, map-filter-reduce
desing principles SOLID, DRY, YAGNI, hollywood principle
  • law of demeter
  • law of leaky abstractions
  • law of unintended consequences

references

[1]
K. Stemmler, “How to learn software design and architecture.” https://khalilstemmler.com/articles/software-design-architecture/full-stack-software-design , 28-Sep-2019.
[2]
E. Gamma, R. Helm, R. Johnson, and J. Vlissides, Design patterns: Elements of reusable object-oriented software. Pearson Education, 1994.
[3]
P. Norvig, “Design patterns in dynamic languages.” http://www.norvig.com/design-patterns/ , 17-Mar-1998.
[4]
S. Wlaschin, “Functional programming design patterns.” https://fsharpforfunandprofit.com/fppatterns/ , Dec-2014.
[5]
Wikipedia contributors, “SOLID — Wikipedia, the free encyclopedia.” https://en.wikipedia.org/w/index.php?title=SOLID&oldid=1237710587, 2024.
[6]
S. Oloruntoba and A. S. Walia, “SOLID: The first 5 principles of object oriented design.” https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design , 23-Apr-2024.
[7]
S. Millington, “A solid guide to SOLID principles.” https://www.baeldung.com/solid-principles , 05-Feb-2019.
[8]
B. Liskov, Keynote address - data abstraction and hierarchy,” SIGPLAN Not., vol. 23, no. 5, pp. 17–34, Jan. 1987.
[9]
Wikipedia contributors, “Circle–ellipse problem — Wikipedia, the free encyclopedia.” https://en.wikipedia.org/w/index.php?title=Circle%E2%80%93ellipse_problem&oldid=1165573623, 2023.
[10]
Y. K. Erinç, “The SOLID principles of object-oriented programming explained in plain english.” https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/ , 20-Aug-2020.
[11]
A. Stec, “Inversion of control.” https://www.baeldung.com/cs/ioc , 16-Feb-2024.
[12]
Wikipedia contributors, “Coupling (computer programming) — Wikipedia, the free encyclopedia.” https://en.wikipedia.org/w/index.php?title=Coupling_(computer_programming)&oldid=1245630908, 2024.
[13]
Wikipedia contributors, “Unintended consequences — Wikipedia, the free encyclopedia.” https://en.wikipedia.org/w/index.php?title=Unintended_consequences&oldid=1365617401, 2026.
[14]
M. Milanović, “Law of demeter.” https://lawsofsoftwareengineering.com/laws/law-of-demeter/ , 2026.
[15]
S. Hudge, “Layers in software architecture.” https://medium.com/@sagar.hudge/layers-in-software-architecture-c8cc16329ff6 , 17-Oct-2024.
[16]
Wikipedia contributors, “Object-oriented analysis and design — Wikipedia, the free encyclopedia.” https://en.wikipedia.org/w/index.php?title=Object-oriented_analysis_and_design&oldid=1230588445, 2024.
[17]
S. Woltmann, “Hexagonal architecture.” https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture/ , 18-Jan-2023.
[18]
B. Okeyo, “A beginners guide to understanding message bus architecture.” https://dev.to/billy_de_cartel/a-beginners-guide-to-understanding-message-bus-architecture-22ec , 24-Apr-2023.
[19]
I. Inc., “Message queue vs message bus: The practical differences.” https://www.inngest.com/blog/message-bus-vs-queues , 29-Jun-2022.
[20]
baeldung, “Layered architecture.” https://www.baeldung.com/cs/layered-architecture , 11-Nov-2021.
[21]
J. Palermo, “The onion architecture : Part 1.” https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/ , 29-Jul-2008.
[22]
A. Cockburn, “Hexagonal architecture.” https://alistair.cockburn.us/hexagonal-architecture/ , 28-Feb-2010.
[23]
R. C. Martin, “The clean architecture.” https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html , 13-Aug-2012.
[24]
Wikipedia contributors, “Model–view–controller — Wikipedia, the free encyclopedia.” https://en.wikipedia.org/w/index.php?title=Model%E2%80%93view%E2%80%93controller&oldid=1244967192, 2024.
[25]
P. Pedamkar, “MVC vs MVP vs MVVM.” https://www.educba.com/mvc-vs-mvp-vs-mvvm/ , 05-Apr-2023.
[26]
Wikipedia contributors, “Leaky abstraction — Wikipedia, the free encyclopedia.” https://en.wikipedia.org/w/index.php?title=Leaky_abstraction&oldid=1314409362, 2025.