Hey friends 👋 This week's newsletter is coming to you from the Flu Party House, which is just like the LAN Party House except there are no games and everyone's got the flu. If something you read today sounds like it was written by a feverish and delusional person, it's because it was.
If you're in the US, I hope you have a fantastic Thanksgiving weekend eating lots of delicious food and hanging out with people who have no idea what TypeScript is. And if you're not in the US, I hope you have a very productive week without all of your American coworkers around—and that you eat lots of delicious food too.
This week, we're talking about architecture testing, architecture thinking, why most projects take forever to ship, and why I have an "office organization" folder in my browser with over 600 bookmarks.
Let's dive in.
SOFTWARE DESIGN
The Beyoncé Rule
A couple of weeks ago, one of my teammates introduced me to the concept of Architecture Testing. He told me how his team was using a Java library called ArchUnit to assert architectural constraints and showed me a few examples of actual tests they were running in CI to make sure the code they wrote was in line with their architecture.
If you haven't heard of architecture testing before, your reaction to this concept might have been similar to mine: “What’s that now? I have to write tests for my architecture? I barely write tests for my code!”
But remember what the authors of Software Engineering at Google called The Beyoncé Rule—“If you liked it, then you shoulda put a test on it.” And we do like our architecture, don't we?
Now, if you don't enjoy writing tests very much (and I wouldn't blame you), I have some good news.
Instead of writing traditional tests, we can set some boundaries and constraints around the most important rules of our architecture to protect them as the codebase grows over time. This way, we can still comply with The Beyoncé Rule, but without having to pull something like ArchUnit into the mix.
Let’s see a few ways in which we can do just that.
ESLint Rules
ESLint is your first line of defense when it comes to testing architectural constraints—it’s fast, runs in your code editor and in CI, and there’s a pretty big chance you already have it installed in your project.
ESLint comes with a bunch of built-in rules that you can use to set some boundaries around your code, like no-restricted-imports, which, despite what its name might be trying to tell you, does in fact restrict imports of certain packages.
For instance, imagine you have an api package that handles your API requests, and you also have some models (like a user model or a shopping cart model) that use the api package to make requests to the right endpoints passing data in the right format.
One of the rules of your architecture could be that your UI components should only make API requests through a model, so you could add a no-restricted-imports rule to your ESLint config that will give you an error if you ever try to import the api package directly.
If ESLint’s built-in rules are not enough to satisfy your thirst for setting constraints, there’s no shortage of third-party plugins you can choose from as well.
A good one is eslint-plugin-boundaries which can help you define more advanced rules about which modules are allowed to import from one another (for instance, restricting imports from an entire family of modules.)
And if you can’t find a plugin that meets your very specific needs, you always have the option of writing your own.
Andrico Karoulla wrote a great three-part series explaining how to enforce design system best practices with ESLint. In his article, he takes an arbitrary rule from the guidelines of a tooltip component in his design system, “Interactive content should not be placed in a tooltip content slot. Tooltips are just meant for showing additional information,” and shows us how we can write a custom ESLint plugin to enforce that rule.
Dependency Rules
For more advanced dependency-checking use cases, take a look at dependency-cruiser. You can set up this library to walk through your entire dependency tree (or only a subset of it) and validate your imports against a set of pre-defined rules.
For example, imagine you’re working on an e-commerce website that you’ve broken down into a series of modules—product module, search module, shopping cart module, and so on.
To keep your modules independent from each other, you might want to define a rule saying that adjacent modules should not depend on each other. For instance, a component in the product module shouldn’t depend on a function defined in the shopping cart module.
There are a few reasons why you might want to enforce a rule like that one, but the main one is that letting adjacent modules depend on each other creates implicit dependencies between them. This could lead to your product details page breaking because of an unrelated change in the shopping cart logic, for example. Not that this exact thing happened to me before, of course.
To prevent these implicit dependencies, you could set up a rule in dependency-cruiser that could look something like this:
Now, next time Maxi someone from your team tries to import a component from a different module, they’ll get an error in CI telling them that what they’re trying to do is against your architectural rules. The error message could also include tips on how to share code across modules instead—like putting the shared code in a shared package.
My favorite thing about dependency-cruiser is that it’s super flexible, so you can use it for a number of different dependency-related use cases. Here are some other things you could do with it:
- Making some dependencies required. For instance, you could set a rule saying that all of your modules with "Provider" in their name must use React's Context API.
- Finding shared utilities that aren’t actually shared. If you want to keep your folder of shared or common utilities nice and organized, it can help you find which files in these folders are used by only one external module, so you can move them closer to the code that actually uses them.
- Finding orphan modules. Orphan modules are modules that are not imported by any other modules, which in most cases means they’re not being used at all and can be safely deleted.
- Deprecating modules. Perhaps there’s an old component or class in your codebase that you don’t want people to use anymore. Dependency-cruiser can help you prevent future uses of it and let people know what alternative they should be using instead.
If you’d like to see examples of how to set up these types of rules on your own codebase, check out the dependency-cruiser tutorial.
Monorepo Rules
So far, we’ve talked about tools for checking the rules of a single project, but if you use a monorepo, you might be interested in checking rules across all of your apps and packages.
One of the most popular monorepo tools out there is Nx, which comes with a number of built-in tools to help enforce module boundaries. You can use these to ban dependencies with certain tags, ban external imports, and more.
The nice thing about these Nx rules is that they’re also ESLint plugins, so if you have the ESLint extension installed in your IDE, you can get feedback about rule violations right in your code editor, without waiting for a CI build to see them fail:
If you use Turborepo or some other type of monorepo tool, you can check out Commonality, a library developed by Alec Chernicki that can enhance your codebase with a nice set of functionality similar to what Nx provides.
In addition to enforcing constraints and dependency rules, Commonality has a Checks API that can help you define certain conditions that your apps and packages must meet. Here are some example rules you can define with the Checks API, stolen directly from their documentation:
- Ensure that all packages have a
README.md
. - Ensure that all
ui
packages have atsconfig.json
that supports JSX. - Ensure that all NPM scripts have a corresponding Turborepo pipeline configured in
turbo.json
. - Ensure that all
vite.config.js
files have matching build configuration for consistent inferred tasks with Nx.
A lot of the rules we talked about today are about managing dependencies and setting boundaries within your application. But there are other things you might want to check depending on what quality attributes your architecture is trying to promote.
For instance, if performance is important to you, you could add a bundle-size check or set up a performance budget that alerts you when your Core Web Vitals drop below a certain point. Or if security is critical for your application, you might want to double down on automated penetration tests and security-related ESLint rules.
The nice thing about The Beyoncé Rule is that it’s not prescriptive. It doesn’t tell you what to test or how to test it, so you can pick and choose whatever works best for your team and your project.
If there’s something your architecture cares about, whether its modularity, encapsulation, consistency, performance, or something else entirely, find a way to stamp a nice CI check on it.
Your team and your future self will thank you for it—and Beyoncé will be immensely proud.
ARCHITECTURE SNACKS
Links Worth Checking Out
- Today's essay was originally going to be about architectural thinking, inspired by Mark Richards's great talk How to Think Like an Architect. As you just saw, I ended up writing about something else entirely, so I'll have to settle for encouraging you to watch the talk and sharing Mark's most important point: "You don't have to be a software architect to think like an architect."
- One of my favorite reads of the week was this fantastic article by Jared Turner explaining why most projects take forever to ship. The illustrations make it painfully clear how easily we fall into the trap of having our most important tasks just sitting there, waiting for someone to move them forward.
- I enjoyed this write-up by Brie Bunge and Sharmila Jesupaul about how their team at Airbnb migrated a large monorepo using Bazel. How large, you ask? Not much really, just a bit over 11 million lines of code.
- Angie Jones came up with a very fitting analogy for what she calls the Betterment metric, which measures someone's willingness to leave the codebase better than they found it. And no, it has nothing to do with Betterment the investment company, so unfortunately you won't find any tips for optimizing your 401(k).
- Speaking of leaving the codebase better than you found it, here's a great article by Tomasz Gil explaining the origins of this principle and sharing a few practical tips for exercising it every day.
- A classic worth re-sharing: How to Build Good Software. "Software is limited not by the amount of resources put into building it, but by how complex it can get before it breaks down." That really hits home, doesn't it?
- I love a good interactive article, and this one by Abhishek Saha illustrating how the browser rendering process works is one of the most beautiful and clearly explained ones I've seen.
- This blog post by Sean Goedecke about how he ships projects at big tech companies made the rounds last week. Some people argue that the post is more about corporate politics than shipping products, but I think his point remains true—shipping a project is about way more than just deploying code to production.
- Sufian Rhazi wrote about why the Shadow DOM is in the front. It helps to think of it as a layer between the user and the plain DOM, instead of as an obscure hidden thing lurking in the shadows as its name might imply. I feel a quote about how naming things is hard is appropriate here.
- I've been on an office organization kick for the past couple of weeks, and this article by Christopher Butler has been a massive inspiration. I doubt my office will end up looking as nice as Christopher's, but I promise to do my best and share some before/after photos when I'm done.
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, LinkedIn, or reply to this email directly with any feedback or questions.
Have a great week 👋
– Maxi