3 Steps to Solve Most Design Problems | How to Find Abstractions Effectively

Last updated Sep 8th, 2022
You can solve nearly any design problem by getting clear on the responsibilities, assigning them to appropriate roles, and figuring out their collaborations.

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.

The Responsibility-Driven Design Process

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.

Coordinator Responsibility Driven Design

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?

Untitled

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.

Untitled

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.

    Untitled

    You can join for free 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.


Discussion

Liked this? Sing it loud and proud 👨‍🎤.



Stay in touch!



About the author

Khalil Stemmler,
Software Essentialist ⚡

I'm Khalil. I turn code-first developers into confident crafters without having to buy, read & digest hundreds of complex programming books. Using Software Essentialism, my philosophy of software design, I coach developers through boredom, impostor syndrome, and a lack of direction to master software design and architecture. Mastery though, is not the end goal. It is merely a step towards your Inward Pull.



View more in Software Design



You may also enjoy...

A few more related articles

How to Learn Software Design and Architecture | The Full-stack Software Design & Architecture Map
Software Design and Architecture is pretty much its own field of study within the realm of computing, like DevOps or UX Design. He...
Organizing App Logic with the Clean Architecture [with Examples]
In this article, you'll learn about the Clean Architecture, why we should separate the concerns of large applications into layers,...
Why I Recommend a Feature-Driven Approach to Software Design
Features represent the essential complexity of software design. It's the complexity that can't be avoided. Everything else — the l...
How I Write Testable Code | Khalil's Simple Methodology
The single biggest thing that improved the quality of my designs was understanding how dependencies influence my ability to write ...