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.
A Shift in Thinking
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.
Design Is Mostly About Change
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.
Principles Before Patterns
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.
How This Changed My Architecture Decisions
Chipset differences do not spread through the codebase.
They live in one place.
OfflineDashcamRepository
▲
│
┌──────────────────┴──────────────────┐
│ │
Gk7201DashcamRepositoryImpl HisiliconDashcamRepositoryImpl
│ │
▼ ▼
Gk7201 API Service Hisilicon API Service
OfflineDashcamRepository
│
▼
Use Case
│
▼
BLoC
│
▼
UI
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.
A Practical Insight I Took Away
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.
Keeping Things Cohesive
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.
A Real Change in Everyday Workflow
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.
What Changed in Me (and What’s Next)
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.

Top comments (0)