🚀Debugging Microservices & Distributed Systems
7 min read

Tips for Reducing Cyclomatic Complexity

Cyclomatic complexity is like counting how many ways a car can go. More options make it harder to drive because you have to make more decisions, which can lead to confusion.

While drinking my morning brew, I came across an interesting topic: Cyclomatic complexity. At first, it was a bit confusing, but then it clicked. Cyclomatic complexity is all about simplifying decision-making in your code.

The more decisions your code makes—through if statements, loops, or conditions—the more complex it gets. Each decision creates a new path, making the code harder to read, maintain, and debug. Cyclomatic complexity measures how many paths your code can take.

Think of it like driving: the more roads you have to choose from at every turn, the easier it is to get lost. In code, each decision point is like another road, and too many roads make it harder to understand and maintain your code.

High Cyclomatic Complexity Leads to Spaghetti Code

As complexity increases, your code can quickly become tangled and hard to follow. This is what we call spaghetti code—code that’s messy, hard to read, and even harder to maintain.

When code is hard to understand, it’s a massive productivity killer. Developers waste time trying to figure out what’s going on instead of adding new features or fixing bugs. You might end up with redundant computations, inefficient loops, or unnecessary API calls that are easy to overlook.

Spaghetti code makes everything slower—debugging, extending, and collaborating. It drags down the team’s productivity and overall progress.

Let’s look at how we can make our TypeScript code easier to follow by reducing cyclomatic complexity. I’ll show you practical examples and explain why they work.

I’m using TypeScript because it’s widely adopted and easy to read, but you can easily apply these concepts to whichever language you prefer.

Break Large Functions into Smaller Pieces

Functions with lots of decision points (if statements, loops) can get complicated fast. Breaking them into smaller, focused functions makes the code easier to read and manage.

Here’s an example of a function with multiple nested if conditions. It’s like having to make multiple driving decisions at every turn, adding complexity to the function.

function processOrder(order: Order): void {
if (order.isPaid) {
if (order.items.length > 0) {
if (order.isShipped) {
// can I finally process??
}
}
}
}

Breaking large functions into smaller ones improves readability and reduces mental load. Each smaller function becomes more reusable and testable because it does just one specific thing.

function canProcessOrder(order: Order): boolean {
return order.isPaid && order.items.length > 0 && !order.isShipped;
}
function processOrder(order: Order): void {
if (!canProcessOrder(order)) {
return; // exit early if the order can't be processed
}
// process the order
}

Instead of wrapping the order processing logic inside multiple if statements, we use a guard clause to return early if the order isn’t valid. This flattens the function, improves readability, and reduces the number of nested decision points.

By moving the logic into canProcessOrder(), we reduce the complexity of processOrder() and make it easier to follow at a glance.

Replace Complex Conditionals with Strategy Pattern

When a function has too many if or switch cases, things get complicated quickly. Each additional case adds another path the code can take, which increases cyclomatic complexity.

function calculateDiscount(type: string): number {
if (type === 'VIP') {
return 0.2;
} else if (type === 'Regular') {
return 0.1;
} else if (type === 'Guest') {
return 0;
} else if (type === 'Admin') {
return 1;
} else {
throw new Error('Invalid type');
}
}

This function has five possible paths, each leading to a different outcome. It’s like deciding between five roads, making the logic harder to follow and maintain. Every time a new discount type is added, the main logic gets more complex and harder to manage.

Every time you add a new discount type, you modify the main logic, which could introduce errors or make the function even harder to maintain.

function calculateDiscount(type: string): number {
if (type === 'VIP') {
return 0.2;
} else if (type === 'Regular') {
return 0.1;
} else if (type === 'Guest') {
return 0;
} else if (type === 'Admin') {
return 1;
} else if (type === 'Student') {
return 0.15; // new case added
} else {
throw new Error('Invalid type');
}
}

Instead of continuing to add more if-else cases, we can use an object lookup or the Strategy Pattern to simplify the logic.

Strategy Pattern

When branching logic grows, it becomes harder to read and understand. Each additional branch requires more mental effort to trace. By using the strategy pattern, the code is cleaner and easier to follow because it reduces the need to manually trace through multiple if statements.

type DiscountStrategy = () => number;
const discountStrategies: Record<string, DiscountStrategy> = {
VIP: () => 0.2,
Regular: () => 0.1,
Guest: () => 0,
Admin: () => 1,
};
function calculateDiscount(type: string): number {
const strategy = discountStrategies[type];
if (!strategy) {
throw new Error('Invalid type');
}
return strategy();
}

By using an object to store strategies, we remove the branching logic from the main function. It’s now easier to add or update discount types.

const discountStrategies: Record<string, DiscountStrategy> = {
VIP: () => 0.2,
Regular: () => 0.1,
Guest: () => 0,
Admin: () => 1,
Student: () => 0.15, // new type easily added
};

Avoid Too Many Parameters (Above 5)

This is one of my favorite rules to follow. Functions with too many parameters are tough to work with and to maintain. It’s a common mistake for beginner to intermediate developers.

function createUser(name: string, age: number, email: string, address: string, phone: string): User {
// create user
}

Grouping related parameters into an object simplifies function signatures and reduces complexity.

interface UserInfo {
name: string;
age: number;
email: string;
address: string;
phone?: string;
};
function createUser(userInfo: UserInfo): User {
// create user
}

By grouping the parameters into a UserInfo object, the function is now easier to work with, especially if more fields need to be added in the future.

Union Types

Union types in TypeScript reduce the need for multiple checks and simplify your code. A union type in TypeScript allows a variable to hold one of several types. In this case, input can be either a string or a number.

This implementation does a runtime check with typeof to determine whether the input is a string or a number, and executes different logic based on the result.

function handleInput(input: string | number): void {
if (typeof input === 'string') {
console.log('Input is a string');
} else if (typeof input === 'number') {
console.log('Input is a number');
}
}

Instead of directly writing string | number in the function, let’s create a type called Input to represent it. Then, we use that Input type in the function.

type Input = string | number;
function handleInput(input: Input): void {
console.log(`Input is a ${typeof input}`);
}

TypeScript knows that input is either a string or number, so we simplify the logic and let TypeScript handle the checks. This version combines both types into one simple check and prints the type directly, treating both types the same.

Avoid Deeply Nested Loops

Deeply nested loops increase cyclomatic complexity because every additional loop adds more decision points. This can make code harder to read and maintain. A good way to simplify is by using helper functions to separate the logic, reducing the nesting and improving clarity.

We have two nested loops: one to iterate through the outer array and another to go through each innerArray. In my opinion, nesting like this is unnecessary complexity.

const outerArray = [
{ innerArray: [1, 2, 3] },
{ innerArray: [4, 5, 6] }
];
for (let i = 0; i < outerArray.length; i++) {
for (let j = 0; j < outerArray[i].innerArray.length; j++) {
// get to da choppa, naow!
}
}

By refactoring the nested loops and moving the innerArray logic into a helper function, we improve the structure of the code. Let’s break it down:

  • Reduce nesting: The outer loop no longer has a second loop inside it, making the flow much easier to follow. Fewer nested loops mean less mental overhead when reading the code.

  • Improve readability: Each function does one thing: handleInnerArray processes the inner arrays, while the main logic just deals with the outer structure. This separation of concerns makes the intent of each piece clearer and easier to maintain.

  • Reusability: The handleInnerArray function can be reused wherever we need to process arrays in the same way, avoiding code duplication and making future updates simpler.

const outerArray = [
{ innerArray: [1, 2, 3] },
{ innerArray: [4, 5, 6] }
];
outerArray.forEach(({ innerArray }) => handleInnerArray(innerArray));
function handleInnerArray(innerArray: number[]): void {
innerArray.forEach(item => {
// get to da choppa, naow!
});
}

This approach helps reduce cognitive load by breaking complex logic into smaller, manageable pieces, and it also reduces cyclomatic complexity, which keeps the code more maintainable in the long run.

Cyclomatic complexity is all about choices. Just like taking too many turns on unfamiliar roads can get you lost, complex code paths can lead to confusion, bugs, and difficult maintenance.


Related Articles

If you enjoyed this article, you might find these related pieces interesting as well.

Recommended Engineering Resources

Here are engineering resources I've personally vetted and use. They focus on skills you'll actually need to build and scale real projects - the kind of experience that gets you hired or promoted.

Imagine where you would be in two years if you actually took the time to learn every day. A little effort consistently adds up, shaping your skills, opening doors, and building the career you envision. Start now, and future you will thank you.


This article was originally published on https://www.trevorlasn.com/blog/tips-for-reducing-cyclomatic-complexity. It was written by a human and polished using grammar tools for clarity.

Interested in a partnership? Shoot me an email at hi [at] trevorlasn.com with all relevant information.