Ever seen code that looks easy to read, but feels hard to understand when you dig into the details? I asked my readers and a bunch of replies came back.
Everyone had a story to share about that time some clean code aficionado smeared logic across a bunch of little functions splatted across dozens of files in five different directories. Hop after abstraction hop before you find the code that does something.
Nothing worse than jumping through 5 abstract abstract wrapper wrappers before you find code that *does* something.
— Swizec Teller (@Swizec) July 26, 2023
This is the tension between two types of complexity.
The in-your-face cyclomatic complexity of complicated code and the subtle architectural complexity that lurks behind intertwined systems with poor isolation of concerns. As an industry we talk plenty about complicated code because it's easy. But it's the complex systems that cause bugs and kill productivity.
Luckily research shows that experienced engineers intuitively understand this and reduce architectural complexity during rewrites. That's what this chapter is about.
Two types of complexity
We use the word "complexity" to describe two subtly different aspects of a codebase. How complicated a piece of code is to detangle and how complex the whole system is to understand.
- Cyclomatic complexity – how complicated is the code
- Architectural complexity – how complex is the system
Cyclomatic complexity counts independent paths through your code. You can think of it as counting the number of nested indents. Most IDEs offer plugins that annotate your code with this metric.
Architectural complexity counts connections between parts of your code, like imports and function calls. The more connections in and out of a file, the more complex it is. Tools exist to visualize this, but you have to go out of your way to use them.
In System Design and the Cost of Architectural Complexity, Sturtevant finds that architectural complexity is the one you have to watch out for because it causes bugs, loss of productivity, and frustrates developers.
Cyclomatic complexity, by comparison, presents barely a road bump to experienced engineers. But it does frustrate beginners.
Cyclomatic complexity
Cyclomatic complexity measures how complicated your code feels. It's the one you notice when you look at a function and go "Wow what a mess?".
If you imagine your code as a control flow diagram, cyclomatic complexity counts decision points between alternative paths. Less than 10 means easy, 50+ means untestable.
The hallmark of complicated code is that it looks gnarly, but has clear entry and exit points. You can step through carefully and figure out how the code works for different input values. This is why it isn't as big a deal as it seems.
But much ink has been spilled about complicated code in our industry because it's easy to talk about. Everyone recognizes complicated code when they see it, beginners (who buy most books) care deeply about it, examples fit on a page, and solutions are easy to prescribe.
Write small functions, avoid deeply nested indents, no global variables. All those are good ideas and we'll talk about them in Chapter 6, but taken too far they make the system harder to understand.
What good is a small easy to understand function, if you need to read 20 of them to understand the full solution to a problem? 1 function that reads like a recipe would've been easier.
That's where architectural complexity comes in.
Architectural complexity
Architectural complexity measures the complexity of your software as a whole. It counts dependencies between areas of a system. More dependencies, more complexity.
A dependency is anything from a function call to a file import. If you're using microservices, API calls between services count and are often the biggest source of tough-to-debug issues.
Architectural complexity is a fairly new metric first proposed in 2013, so not a lot of tooling exists yet. There are codebase visualizers like Madge and UML diagramming tools that can visualize file imports. Most don't go down to the level of function and API calls. They also tend to ignore transitive dependencies.
But tools like Madge can give you useful insights despite their limitations. Here's an example from a large React app.
Clearly there's a lot going on. You can identify a couple utility files on the right, that's a smell. Top left you can see 2 files with a circular dependency, another smell.
If you squint a little, you can see areas where many files depend on each other with few incoming and outgoing dependencies. That tells you there's a module hiding in there trying to break out. This is where you slice during a rewrite.
NOTE: Madge isn't ideal for identifying file neighborhoods because it doesn't try to visually group related files. Different layout modes exist, but none that feel quite right for finding neighborhoods.
Academics use DSM diagrams to better visualize this in their papers, but those are hard to generate. Looks like this:
The more you organize your code to make those modules explicit, the easier it is to move your code around.
Here's a followup post with answers to reader questions 👉 Followup answers to Forget complicated code, focus on the system
Continue reading about Forget complicated code, focus on the system
Semantically similar articles hand-picked by GPT-4
- Two types of complexity and their impact
- You can't fix the wrong abstraction
- Followup answers to Forget complicated code, focus on the system
- Why taming architectural complexity is paramount
- Finding modules in a Big Ball of Mud with ChatGPT
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️