Cascading Logic

By Joe Hicks, 2026-03-10

The Problem With Being Clever

I want to talk about a coding pattern I've been using for years that I call "cascading logic." It doesn't have a formal name because, as far as I know, I made it up. It came out of years of writing decision-heavy code in my ecommerce work, and I think it deserves a name because it solves a real problem that I see developers struggle with.

The problem is this: you have a function that needs to evaluate a bunch of data and arrive at a set of decisions. The decisions are related, sometimes dependent on each other, and the business rules change every quarter. The instinct — especially for newer developers — is to optimize. Break it into small functions. Reduce the loops. Eliminate redundancy. Make it DRY.

And that instinct is usually wrong for this kind of problem.

The Pattern

Cascading logic is simple. You initialize all your target variables upfront, set them to null, and then fill them in one at a time through sequential, independent blocks of logic. Each block has its own loop and its own conditions. Later blocks can reference earlier results naturally, because everything shares the same scope.

Here's what it looks like. Say you're selecting shipping rates for a customer:

let cheapestGround = null;
let fastestGround  = null;
let bestExpedited  = null;

// find the cheapest ground rate
for (const rate of rates) {
    if (rate.type === "ground") {
        if (!cheapestGround || rate.price < cheapestGround.price) {
            cheapestGround = rate;
        }
    }
}

// find the fastest ground rate that beats the cheapest
for (const rate of rates) {
    if (rate.type === "ground" && cheapestGround) {
        if (rate.days < cheapestGround.days) {
            if (!fastestGround || rate.days < fastestGround.days) {
                fastestGround = rate;
            }
        }
    }
}

// find the best expedited rate
for (const rate of rates) {
    if (rate.type === "expedited") {
        if (!bestExpedited || rate.price < bestExpedited.price) {
            bestExpedited = rate;
        }
    }
}

Three loops. "Redundant" iteration. A junior developer would look at this and immediately want to collapse it into one pass. But look at what we get: each block reads like a plain English requirement. "Find the cheapest ground rate." "Find the fastest ground that beats the cheapest." "Find the best expedited rate." Done.

The Payoff

Now here's where it gets good. Business comes to you and says: "Don't show the expedited option if it's actually slower than the fastest ground option."

With cascading logic, this is a few lines inserted between existing blocks:

// reject expedited if it's no faster than ground
if (bestExpedited && fastestGround) {
    if (bestExpedited.days >= fastestGround.days) {
        bestExpedited = null;
    }
}

No restructuring. No refactoring. No tracing through nested conditionals to figure out where this check belongs. The new rule just slots in, exactly where it makes sense in the decision flow. Every variable it needs is already populated and in scope.

This is the payoff of cascading logic: changes map 1:1 to new blocks or simple checks between existing ones.

"Why Didn't You Break This Up?"

I've had other developers question why a function using this pattern is so long. "Shouldn't this be broken into smaller functions?" It's a fair question on the surface. But when you actually look at the code, you see that a variable set in one tiny case near the top is referenced in another tiny case way down the decision path. The whole function is one complete picture — the full breadth of variables all need to be in scope because they're all considered for the final decision.

Breaking it up doesn't simplify anything. It just scatters the logic across files and forces you to pass a dozen parameters around — or worse, create some state object that's really just a worse version of local scope. You've traded a long function you can read top-to-bottom for a scavenger hunt across abstractions.

Embrace Redundancy

The key insight is that cascading logic embraces redundancy on purpose. Instead of trying to be clever with a single pass or complex nested conditionals, you write multiple simple passes. Each one does exactly one thing.

Compare this to the "optimized" alternatives:

One loop with complex conditionals — Harder to read, harder to modify. Every new rule means touching existing conditions. Good luck figuring out the order of operations six months from now.

A scoring or ranking abstraction — Now you're maintaining a framework instead of writing business logic. Over-engineered for a problem that just needed some if-statements.

Cascading logic trades brevity for clarity and maintainability. Each block is dead simple on its own, and you can reason about the entire decision flow just by reading down the page.

When To Use It

This isn't a pattern for everything. It shines when you have a decision function that:

  • Evaluates the same dataset for multiple related outcomes
  • Has rules that reference each other across those outcomes
  • Changes frequently as business requirements evolve

In my ecommerce work, shipping logic and pricing functions are the perfect home for this. The requirements are complex, they change often, and getting them wrong costs real money. Cascading logic lets me add or modify rules with confidence, because I can see exactly where the new logic fits and exactly what state it has access to.

Write It Like You'd Say It

If I had one takeaway, it would be this: logic should be as verbose as possible, as close to human language as possible. Optimizing for fewer lines of code is not the same as optimizing for maintainability. Sometimes the best thing you can do is write more code, not less — code that reads like a checklist of business requirements rather than a clever algorithm.

Junior devs especially tend to optimize as soon as possible, not realizing they are adding complications even though the result might be less code. Cleverness has a maintenance cost. Simplicity pays for itself every time someone needs to change the logic — including future you.

The next time you're building a complex decision function, try cascading logic. Initialize your variables. Fill them in one block at a time. Let the later blocks lean on the earlier ones. When the next requirement drops in, you'll thank yourself.