How “Design Patterns Explained” Changed the Way I Design Software

I build apps, internal tools, and sketch stuff on my iPad. Into side projects, and occasionally sharing thoughts here.
Search for a command to run...

I build apps, internal tools, and sketch stuff on my iPad. Into side projects, and occasionally sharing thoughts here.
This is a great breakdown of how design patterns shift the focus from 'making it work' to 'making it maintainable.' I’ve found that the real shift happens when you stop seeing patterns as just theoretical structures and start seeing them as solutions to specific 'code smell' scenarios.
For anyone currently digging into these patterns, I’ve been working on a tool that summarizes technical deep-dives and video documentation into concise, readable formats. It’s been helping me get through architectural documentation much faster—you can check it out at ytskim.com.
Out of curiosity, which pattern did you find had the steepest learning curve when you first started applying it to production code? For me, it was definitely getting comfortable with the Strategy Pattern versus just relying on overloaded conditionals.
I don't remember which pattern had the steepest learning curve for me, but the Bridge Pattern certainly took a while to click. It has quite a few moving parts, and the naming conventions can be a bit confusing when you're first learning it.
My usual approach is to look at lots of examples, then map the implementation to a UML diagram so I can clearly identify the different components and their responsibilities.
After that, I try to implement it myself as an exercise. The real test, though, is being able to recognise when a pattern is actually needed while designing or refactoring code. That's usually when the pattern truly starts to make sense.
The Bridge Pattern really resonates with me. I had a similar experience with Strategy vs just using conditionals everywhere. The book frames it as "what varies vs what stays the same" which sounds simple but it took me a while to actually see it in my own code. The moment I started asking "what's going to change here in 6 months" before writing a new module, the patterns just showed up naturally. Didn't have to force them.
"What is stable, and what is likely to change?" is probably one of the most useful architecture questions there is. We've been seeing something similar in BXRuntime recently. Several systems evolved independently, but once we started isolating what changes over time, a very different architecture emerged than the one we originally planned.
Great read.
Thoughts on designing, building, and maintaining software. Topics include design patterns, testing, architecture, code quality, and the lessons learned while becoming a better engineer.
I used to think of tests as just a final check to confirm whether a feature worked. That perspective shifted a few years ago when I had shipped a feature, and later a teammate made changes that uninte
Human Learning ML — Devlog #1

I used to think of tests as just a final check to confirm whether a feature worked. That perspective shifted a few years ago when I had shipped a feature, and later a teammate made changes that uninte

A sudden AI interest in my company has left me avoiding it as much as possible. I’ve been using Copilot for 3 years now and have been very happy with it. When ChatGPT launched, I quickly created an ac

Introduction Hey there! I'm trying out something new today. This is not a tutorial blog but more of a personal development blog.It has no conclusion or takeaway if that's what you're looking for. This

I read Design Patterns Explained 2nd Edition a few months ago. I thought I'd finally write about it and some of the lessons I took away from it.
I was reading the final chapters while starting Offline Dashcam, a project at Wheelseye, a logistics company. Looking back, it was the perfect project to apply many of the ideas from the book.
This article is about how the principles in the book helped me design and code a complex feature.
There are books that give information or are used as references. But sometimes you read a book that changes the way you think.
Design Patterns Explained is one of those books for me. The book is for anyone who is looking to understand design patterns, not just catalog them, but actually understand why they exist, what problems they solve, how to recognise, implement, and refactor them.
It explained encapsulation through tons of examples, along with concepts like abstraction, polymorphism, and inheritance. More importantly, it showed how these ideas influence real design decisions: which classes should be abstracted and how classes should relate to and aggregate one another.
As engineers, we know features evolve. A “simple” requirement today can grow into something much bigger tomorrow.
I always tried to be ready for that. But I sometimes focused on the wrong kind of change. Things worked, they shipped — but updating them wasn’t always smooth. I knew I could improve how I approached design, but I couldn’t clearly explain what was missing.
I didn’t read Design Patterns Explained to solve this. I picked it up just to understand design patterns better.
But while reading, I started noticing the missing pieces in my own decisions.
And those realisations changed how I design software today.
This book didn’t teach me patterns. It changed how I think. And once that happened, patterns showed up naturally in my code.
One idea hit me early in the book:
Build for today. But keep the door open for tomorrow.
My usual flow was:
Understand the feature
Code it
Adjust later
It wasn’t wrong. It just wasn’t always ready for the right changes.
The Offline Dashcam is a device with a built-in wireless hotspot and an SD card slot. It is attached to a vehicle and records video to the SD card. We can connect to its Wi-Fi to view, download, and delete those recordings. I was working on adding support for this feature in our mobile application (built using Flutter 💙).
Each vendor has different firmware with different chipsets. This means:
Different API endpoints
Different request bodies & responses
Different capabilities
We need to keep our codebase open to adding new chipsets, while maintaining the capabilities of each one without disrupting user experience.
Encapsulate what varies.
Before reading the book, I would often think in terms of features. The book pushed me to think in terms of change. Instead of asking "What does this feature do?", I started asking "What part of this feature is likely to change independently?"
I created a list of things that were likely to vary:
The chipset initialisation process is different for each chipset.
The API endpoints and response protocols are different.
Capabilities of each chipset are different. (E.g. audio support)
What stayed the same was more interesting:
Start Recording
Stop Recording
Delete File
Get Device Info
Get Recordings
Take Snapshot
No matter which chipset the user connected to, the app still needed to perform the same operations.
That led me to a simple question:
What if the rest of the application only knew about these operations and never about the chipset itself?
I introduced a common repository contract:
abstract class OfflineDashcamRepository {
Future<void> startRecording();
Future<void> stopRecording();
Future<DashcamMedia> getDeviceData();
Future<DashcamDeviceInfo> getDeviceAttributes();
}
Each chipset implemented the same contract in its own way, allowing the rest of the application to remain unaware of vendor-specific details.
This confirmed that each chipset should have its own repository and API layer. The rest of the layers should not be aware of these differences. The initialisation process also varied by chipset. To keep that complexity from spreading through the application, I introduced a factory that selects the correct repository implementation during onboarding. From that point onward, the rest of the application works through a common abstraction.
I explored different approaches, applied the "encapsulate what varies" principle to each of them, and chose the design that best matched the kinds of change I expected.
Before, chipset differences leaked into business logic:
if (chipset == Chipset.gk7201) {
final response = await gkApi.getDeviceAttributes();
...
} else if (chipset == Chipset.hisilicon) {
final response = await hisiApi.getDeviceAttributes();
...
}
Every new chipset meant more conditionals scattered throughout the codebase.
Instead, the chipset is detected once during onboarding:
final chipset = await chipsetDetector.detectChipsetType();
final repository =
dashcamRepositoryFactory.create(chipset);
From that point onward, the rest of the application talks only to the repository abstraction:
final deviceInfo =
await repository.getDeviceAttributes();
The use cases, blocs, and UI never need to know which chipset is connected.
Chipset differences do not spread through the codebase.
They live in one place.
The chipset-specific logic exists only in the API and repository layers.
Everything above the repository layer depends on abstractions. Adding a new chipset means creating a new implementation, not rewriting the UI, BLoCs, or use cases.
At runtime, the correct implementation is selected.
The rest of the system doesn’t need to know which chipset it is talking to.
When I stopped thinking “which pattern should I use?”
and instead started asking:
What here is stable, and what is likely to change?
In this case, chipsets changed. The operations the app needed to perform did not.
Now I look for variability by asking questions. I interrogate new code: Does this depend on specific hardware? Will another vendor break this? Can the UI team change a colour without me refactoring the API layer? If the answer is yes, that piece gets its own box. Its own boundary. Its own file where it can change without asking permission.
Earlier, I sometimes built giant “god” BLoCs or Services that handled everything.
But now, I have use cases, which delegate to services, factories, repositories, etc., as needed.
Now:
UI layer handles UI stuff
Storage handles storage stuff
Hardware/API code stays near hardware/API code
No giant boss-component running the whole show.
If a new engineer joins later, they won’t have to decode one huge file to make a small change.
High cohesion → less stress.
Earlier, if a new requirement came in — for example, “Also support taking snapshots while recording” — I would hesitate a bit.
Where should that logic go? Will it break recording flow? Will I end up refactoring too much?
Now, the path is clear:
A new feature → a new use case
Hardware behaviour → a new chipset implementation
API change → stays near the API service
UI behaviour → stays in its own layer
Refactoring and testing became more predictable because each piece has its own space.
When code has the right boundaries, future change stops feeling risky.
I didn’t expect this book to influence my day-to-day work.
But I caught myself designing with more clarity from the very next feature.
Now, I:
notice variability early
keep change local
choose simplicity by default
and think about the developer who joins after me
I’m still learning to balance all this, but my software is starting to feel more ready for whatever comes next.
If you’ve ever felt your code works but it doesn’t feel quite ready for the future, this book might help you spot the same gaps I did.
I picked up the book hoping to learn design patterns. I finished it thinking differently about change. The patterns were useful, but the mindset shift was what stayed with me.