Don't Make Me Think

Cognitive load, cohesion, simplicity, micro-frontends, naming things, and how selfishness is the key to good component design.

Issue 8 /

Hey there,

Do you have plans for this upcoming Friday, October 27th, at 9:30am Pacific? Work, you said? I'm not entirely sure what that word means, but I hope you can make some time come chat about application design in Astro with Ben Holmes and me on my Twitch channel.

This is the first of what I'm hoping will become a series of talks on frontend architecture and software design with different framework authors and experts, and I couldn't be more excited to kick things off with Ben. It'll be a lot of fun, so I hope you can join us for a little bit!

Today's issue is packed with goodies; we'll talk about cognitive load, cohesion, simplicity, micro-frontends, naming things, and how selfishness is the key to good component design. Let's dive in.


Don't Make Me Think

Character illustration generated with DALL-E

Back on issue #1 of the newsletter (we were so young), we talked about the causes and symptoms of complexity and their importance to the overall health of your codebase.

One of the symptoms we discussed is cognitive load, which is the amount of information you need to keep in your head at a time to make a change. My advice for reducing cognitive load in your codebase was to keep your modules cohesive, which sounds great and all, but also… what does it even mean?

I owe you a more detailed explanation, so today's issue is all about cognitive load and what cohesion has to do with it. In the famous words of every PM you ever worked with, let's double-click on that.

Cohesion and Cognitive Load

Let's start with an example.

Imagine a FileUploader component that is responsible for (surprisingly) uploading files, but also validating file types and checking user permissions. Some users can't upload files at all, others can only upload certain file types, and so on.

Now, imagine we need to change the validation rules to add a new allowed file type. There is no sign of any validation logic in the FileUploader component, so we put our detective hat on and start looking for clues to find where we should make our change.

In our example codebase, there's an API service that handles network requests, including requests to the file-uploading endpoint. This sounds like an appropriate place to put file validation, so we follow that path. Then, we discover that permissions and validation are handled by different modules that call each other as part of some complicated logic, so we need to follow those paths as well.

After a while, we find all the different places we need to change to add our new validation rule, and we're able to finish our task. But we paid a high cost for it—the change took longer than it should have, and we ended up feeling exhausted from having to put together this puzzle in our heads.

Changing the validation rules requires us to understand how a bunch of different services work together

This is what high levels of cognitive load feel like. Jumping from module to module until we finally find the place (or, more likely, places) where we need to make our change. At each step, we have to make sense of the module we're currently looking at, load all of its logic in our heads, and figure out where we should go next.

There are many ways to reduce cognitive load, but one that would work particularly well for this example is to declare the file type and permissions relationship closer to the component that cares about them. In practice, this could look like a simple map:

Data structure representing our validation and permissions rules

This doesn't simplify the relationship between our services in any way, but it does wonders for reducing cognitive load. We might still have some complicated logic between our different services, but now we don't need to concern ourselves with it. We can now declare our updated rules without having to load a bunch of information in our heads.

Changing the validation rules is much easier now; we don't need to think about the complicated logic powering this feature

Our updated design is simpler because it's more cohesive—things that belong together, are together. By bringing the file-type validation and permissions closer to the FileUploader component, we're bringing the file-uploading concerns of our application closer together, which makes them easier to think about.

We've also made our design more declarative. The file validation and permissions relationship was previously represented as a bunch of complicated logic. Now, we've replaced that relationship with a simple data structure, which is both easier to change and easier to understand than any action or operation.

Making things cohesive isn't always as simple as in this example, so here are a few other things you can do to reduce cognitive load:

  • Use descriptive names: understanding what something is or does by simply looking at its name is one of the best ways to reduce cognitive load. Naming things is hard, but it's worth spending a bit of extra time to come up with a good one.
  • Write good comments: don't write comments that repeat the code, but use them to explain relationships and decisions you've made. When naming isn't enough, use comments to describe complicated logic in simple terms.
  • Document high-level decisions: if what you need to explain doesn't fit in a code comment, or if it spans multiple modules, it's a good idea to spin up a quick document explaining the tricky parts of the implementation.

Reducing cognitive load is an effective way to simplify your codebase, but more importantly, it can be a significant quality-of-life improvement for the people who work in it.

It takes time and discipline, but it's definitely worth the investment. Don't make the users of your codebase think too much (remember, this includes both your team and your future self), and they will certainly thank you for it.


Simple Made Easy

If there is one talk that truly grasps the essence of simplicity vs. complexity (which is most of what we talk about in the newsletter), it has to be Simple Made Easy.

Rich Hickey, a long-time functional programmer and creator of Clojure, gave this talk at StrangeLoop over a decade ago, but everything he talks about is as relevant today as it was back then.

The focus of the talk is on the difference between simple (when things have one role or cover one concept) and easy (things that are familiar, or "at hand"), and how the reason we end up with complex systems is that we tend to prioritize things that are easy, even if they bring a lot of complexity with them.

There are too many amazing lessons in this talk and I could never do them justice in this short section, so I highly recommend you give this one a watch, take notes, and bookmark it to re-watch at least once a year. It is that good.


Links Worth Checking Out

If you're confused about React Server Components, you're definitely not alone. This website gives us a series of interactive resources to make them easier to understand and implement.

This is a great write-up on component composition and avoiding complicated props using "self-interested component design."

A detailed comparison of the features Next.js 13 and Remix, building a Twitter Clone app in both frameworks.

An optimistic view of micro frontends and its potential to solve some of the most serious issues of large monolithic applications.

We just talked about the importance of good naming for reducing cognitive load, didn't we? This article goes deep into that topic, explaining how better names can also lead to better code.


Building Micro-Frontends

Building Micro-Frontends by Luca Mezzalira

Luca Mezzalira has been writing and speaking about micro frontends for as long as the term has been around. You can learn a lot about this architectural style from the articles and conference talks he's given over the years, but if you're looking for a comprehensive guide, look no further than his 2021 book Building Micro-Frontends.

One of the core concepts in the book is the Micro-Frontends Decision Framework, which you can use to figure out which flavor of micro frontend architecture is right for your application. This includes deciding how to split your micro frontends (vertically or horizontally), how to compose and route them (on the server, client, or at the edge), and which methods they'll use to talk to each other.

You'll also find case studies, tips for introducing micro frontends to your organization, and strategies for making the migration from a monolithic architecture. It really covers a lot of ground, so I'm sure you'll find it useful whether you're using micro frontends today, or just want to learn more about their pros and cons.

That’s all for today, friends! Thank you for making it all the way to the end. If you enjoyed the newsletter, it would mean the world to me if you’d share it with your friends and coworkers. (And if you didn't enjoy it, why not share it with an enemy?)

Did someone forward this to you? First of all, tell them how awesome they are, and then consider subscribing to the newsletter to get the next issue right in your inbox.

I read and reply to all of your comments. Feel free to reach out on Twitter or reply to this email directly with any feedback or questions.

Have a great week 👋

– Maxi

Is frontend architecture your cup of tea? 🍵

Level up your skills with Frontend at Scale—your friendly software design and architecture newsletter. Subscribe to get the next issue right in your inbox.

    “Maxi's newsletter is a treasure trove of wisdom for software engineers with a growth mindset. Packed with valuable insights on software design and curated resources that keep you at the forefront of the industry, it's simply a must-read. Elevate your game, one issue at a time.”

    Addy Osmani
    Addy Osmani
    Engineering Lead, Google Chrome