3 Steps to Solve Most Design Problems | How to Find Abstractions Effectively
When I was in university, I decided to build my first startup with a couple of buddies that helped students find jobs.
Having spent a year learning JavaScript and flying through Tyler McGinnis’ courses on React and Redux, I went in.
Slinging code like wild, I eventually encountered a challenge: to design a complex page-routing system that would re-route users to the correct pages depending on a variety of different factors (state, user type, account verification).
Since I couldn’t find a library to help me do this, I had to build it myself.
Feeling overconfident, I over-engineered the heck out of it with no tests.
Over the rest of the lifetime of the project, that routing system became the key area to cause bugs, frustrating customers and humbling me in the process.
Ah, the art of creating good abstractions.
How I wish I knew back then what I know now.
Let’s talk about how to solve most design problems and find abstractions effectively.
The Responsibility-Driven Design method
I’ve explored many different ways to design software but if you ask me, I’d say that learning Responsibility-Driven Design was the last thing that I really needed to deeply understand the nature of design.
The Responsibility-Driven Design process is pretty straightforward, but the magic is in the details.
Here’s how it works.
Start with a requirement (functional or non-functional), convert it into responsibilities, assign those to roles, then find the collaborations.
Allow me to demonstrate using a call I had with a dev last week as an example.
Step 0: Identify the requirement
Say you’re working on some back-end code.
You’ve been asked to add a feature.
The feature is: if you perform a request while logged in, the system should update your account’s lastActive time.
Step 1: Decompose the requirement into responsibilities
First things first.
Understand that requirements contain a multitude of smaller responsibilities and responsibilities.
Responsibilities can be for either doing or knowing.
Here’s what we did. At first, we found the following responsibilities.
For doing
- Update the last active time
- Intercept requests
For knowing
- Know if the user is logged in or not
But then, after further questions about performance (ie: like how do we do this really fast if we have to go to the database all the time — wouldn’t that be inefficient), we identified even more responsibilities. Responsibilities involving caching.
The working list of responsibilities changed a bit more:
For doing
- Intercept all requests
- Update the last active time in the cache
- Persist last active time in cache to the database periodically
For knowing
- Know if the user is logged in or not
- Know when to persist to the database
Not bad so far.
Step 2: Look for common object (role) stereotypes
The next thing we do is take your list of responsibilities, and for each one, assign them to roles.
If you forgot, a role is a collection of responsibilities. For example, a Repository usually has the responsibility to save and find domain objects.
Cool, but how do we come up with roles? How do we know which responsibilities to assign Is there a structured way to do this?
There is.
You use the six Object Stereotypes.
Check that article out and come back.
Back?
Sweet.
So yeah, object stereotypes are probably the best part of RDD. They act as building blocks — stereotypical elements — of any design.
After we got up to speed on object stereotypes, when we looked at the intercept all requests responsibility, I asked:
“What kind of stereotype do you think handles something like intercepting requests?”
Almost immediately, we realized this behaviour as something that a Coordinator (the fourth stereotype) would do.
Coordinators typically only have one real purpose: to pass information to other objects so that they can do work.
Next question. Do we need to create a new coordinator or is there already one present we could merely extend?
Me: “So, do you have already anything in your architecture that acts like a coordinator?”
Him: “Hmm. Actually, yeah. I’m using Express. You know, as my router. It has this middleware feature in it. I could probably use that, right?”
Me: “Absolutely.”
Amazing. We already have a coordinator. It can handle the intercepting responsibility.
That’s 1 out of 5 responsibilities assigned to roles.
We continue going down the list and assigning responsibilities to roles.
Step 3: Collaborations
After you’ve assigned your responsibilities to roles (ideally based on Object Stereotypes), you’re going to need to figure out how to connect your new or existing roles together.
It’s time to identify the collaborations.
There are number of ways to do this, but the most obvious way is to — again, look back to the stereotypes.
If you know:
- The object stereotype
- Where it lives within in a layered architecture
Then you can usually figure out who its neighbour's are. We can ask ourselves:
- Who does it need help from?
- Who does it help?
After assigning the responsibilities to roles, we started fleshing out the collaborations:
- For intercepting all requests → We identified the Express Middleware (Coordinator) and Coordinators typically communicate with Controllers. Therefore, we realized we needed UpdateLastActive use case (Controller).
- For updating the last active time in the cache → We had to invent a Cache (both Interfacer & Structurer stereotypes). This was to be used by the UpdateLastActive use case (Controller).
A tricky one was the responsibility of persisting the last active time in cache to the database periodically.
For this, we had to do break it down even further. The idea of periodic updates implies schedules which implies scheduling responsibilities. We had to:
- Find all the scheduling responsibilities
- and assign them to new and existing roles
Eventually, we ended up with:
- a Scheduler (both the Service Provider and Coordinator stereotypes)
- a ScheduledTask (Controller)
- and of course the UserRepo (Interfacer) to actually update the user’s info
Boom. Done.
A quick little drawing to make sure things actually line up and we’re off to writing the first tests and coding it out.
Patterns, communication & abstraction
Do you see how powerful this is?
By adopting a design method and levering the fact that we, as humans, are pattern-matching machines, you not only gain the ability to solve problems with well-known solutions, you find it incredibly easy to communicate your design ideas with others.
Without needing to get into the details, as soon as we established what a Coordinator was, the developer was able to identify it within his architecture instantaneously.
Abstraction. It’s what Alan Watts taught us.
Archetypes. It’s what Carl Jung knew.
If you were a writer, you’d leverage the Jungian personality archetypes to write engaging stories and plays.
But you’re a developer, so familiarize yourself with object stereotypes and learn to write scalable software.
Learn to cook, don’t just mix
Once, my mom visited me in university to check in on me and how I was cooking for myself.
Noticing the empty Kraft dinner boxes, I heard her say:
“Kraft Dinner isn’t cooking; that’s just mixing.”
I apologize to any Kraft dinner chefs out there reading this, but she’s right.
Doesn’t matter if you’re frontend or backend, when you’re stuck in library and framework-land, you don’t learn how to design and you don’t learn how to architect.
You just mix.
When all you have are the things that libraries and frameworks give you, you’re pretty darn limited in your thinking.
It’s always gonna be roles, responsibilities, and collaborations
I presented an image earlier. I’ll show it again here.
This image depicts pretty much everything you’ll ever come across in building web applications.
I recommend you screenshot it or save it.
Doesn’t matter if you’re building robots, video games, or something else — your task is always the same: assign responsibilities to roles and design collaborations.
Even if you’re doing pure (and proper) functional programming. Same task.
To me, Responsibility-Driven Design is as deep as you need to go. It contains foundational ideas that help you:
- Develop a second sense for how to solve design problems
- Learn how to design scalable frontend and backend architectures
- Identify the patterns and stereotypes within your favourite libraries and frameworks, rely on them less, use them more effectively, and fill in design gaps with your own abstractions when they try to lead you astray to do things that would render your code untestable
- Venture into how to design distributed systems
I hope that was helpful.
You’ll hear more about the important parts of RDD and how to design scalable frontend architectures from me in the future.
Let me know if there’s something you’d like to know specifically.
—
Until then, enjoy the rest of your week.
To mastery,
Khalil
—
What happened this week
- The Frontend Architecture Academy — This just in. An 8-week 1-on-1 coaching program where I teach you how to build scalable frontend applications using the best parts of DDD, TDD, BDD, RDD, clean architecture and more. Slots are limited. If you’re interested in joining, want more info, or could simply use some advice, throw some time on my calendar. I’d love to hear what your challenges are and to see if I can point you in the right direction.
-
Phronesis Labs Discord community — Well folks, I’ve finally done it. The advanced software design and architecture discord community you’ve been asking me to make for about 2 years is finally here.
- Writing a song on flow, Dionysus, and the creative process (demo) — In The Birth of Tragedy, Nietzsche wrote that the struggle of human nature is that we are both Apollonian (see the Greek god of light and reason) and Dionysian (see the god of wine, ecstatic emotion, and tragedy). Things like music, art, passion, and love pertain to the realm of Dionysus. Sculpture, craftship, and progress pertain to the Apollo. Whenever I sit down to write (music, books, blog posts), I find myself transported to the realm of the Dionysus — perhaps the very same realm that Mihaly Csikszentmihalyi calls Flow. It’s the optimal state of consciousness. Of happiness. You can listen to the rough demo here.
Stay in touch!
Join 20000+ value-creating Software Essentialists getting actionable advice on how to master what matters each week. 🖖
View more in Software Design