Learning Scala: Why Pattern Matching Matters
By this point in the series, we've spent a lot of time making the mechanics of code disappear. We stopped mutating values because mutable values drift. We stopped depending on external state because it breaks determinism. We stopped writing statements because expressions are clearer, safer, and easier to compose. And, most recently, we stopped writing loops because functional transformations let the business logic stand on its own.
But there is another way imperative languages obscure meaning — and this one is subtler. It happens any time a developer tries to figure out what kind of thing something is before deciding what to do with it.
In Java, this shows up everywhere:
if (type.equals("percentage")) …
if (instanceof DiscountRule) …
switch(type) with a long list of string cases
deep inheritance chains built just to support branching
boolean flags like isBOGO or requiresThreshold
configuration objects stuffed with fields that only matter sometimes
All of these are symptoms of the same underlying issue: The shape of the data is not expressed in the type system. And because the type system doesn’t know the shape, the compiler can’t help you handle it.
Pattern matching is how Scala, and functional programming more broadly, fixes this. It restores clarity not by offering a nicer way to write an if/else, but by giving developers a way to express domain structure directly, in a way the compiler can reason about.
In digital commerce, where the shape of a promotion, a return, or a shipment directly determines how the system behaves, this becomes not just a stylistic improvement, but a correctness guarantee.
Catch up on earlier posts to follow along with the Functional Programming Isn’t Just for Academics series:
- Post 1: Why Functional Programming Matters for the Systems We Build Today Post 2: Immutability by Default and the Foundation of Reliable Systems Post 3: Pure Functions: Your First Step Toward Bug-Free Concurrency Post 4: Thinking in Expressions Instead of Statements Post 5: Beyond the For-Loop with map, filter, and flatMap
Each installment dives deeper, illustrating how Scala enables teams to write clean, testable, and scalable applications.
Where Imperative Branching Breaks Down
Let’s take promotions, which are never as simple as they sound. Here are just a few common rules:
A percentage discount on eligible items
A fixed amount off
Buy X Get Y of equal or lesser value
Free shipping over some threshold
Category-based discounts
Brand-specific incentives
Tender-based discounts (e.g., “10% off when paying with store card”)
Imperative systems typically model this using a combination of:
a “type” field,
a big enum,
an inheritance hierarchy,
configuration flags, and
long branching structures.
Let’s look at a common Java approach:
public Money applyDiscount(PromotionRule rule, LineItem item) {
if (rule.getType().equals("PERCENT")) {
return item.getPrice().multiply(rule.getPercent());
}
else if (rule.getType().equals("AMOUNT")) {
return rule.getAmount();
}
else if (rule.getType().equals("BOGO")) {
if (item.getQuantity() >= rule.getBuyQty()) {
return item.getPrice().multiply(0.5);
} else {
return Money.zero();
}
}
else if (rule.getType().equals("FREE_SHIP_OVER")) {
if (cart.getTotal().greaterThan(rule.getThreshold())) {
return Money.zero();
} else {
return Money.zero();
}
}
// And so on…
throw new IllegalArgumentException("Unknown rule type");
}
This is normal Java. It is also fundamentally brittle. Every time a new rule type is added:
the branching logic grows
the chance of forgetting a case increases
rules that don’t apply in a given context still appear in the code
the compiler cannot enforce exhaustiveness
business meaning is spread across mechanics, flags, and runtime checks
and “illegal states” (like a BOGO rule without a buy quantity) remain representable
By the time a business has 20–30 promotion types — which is common — the code becomes a wall of conditionals. This isn’t because the developers did anything wrong. It is because the language gave them no other way… But pattern matching gives them another way.
Modeling Promotions as Algebraic Data Types
Instead of encoding variety through enums, flags, or base classes, Scala models domain variation explicitly.
sealed trait PromotionRule
case class PercentageOff(percent: BigDecimal) extends PromotionRule
case class AmountOff(amount: Money) extends PromotionRule
case class BuyXGetY(buyQty: Int, freeQty: Int) extends PromotionRule
case class FreeShippingOver(threshold: Money) extends PromotionRule
A few things happen immediately:
The structure of the domain is expressed in the type system.
Illegal states become unrepresentable — e.g., a percentage rule must have a percent.
The compiler knows every possible promotion rule.
The business logic can now be written as pattern matching over these shapes.
This shifts complexity from runtime to compile time — the safest place for it to be.
Pattern Matching: Expressing the Business Rule Directly
Here’s the same promotion evaluation rewritten in Scala:
def applyDiscount(rule: PromotionRule, item: LineItem): Money =
rule match {
case PercentageOff(p) =>
item.price * p
case AmountOff(a) =>
a
case BuyXGetY(buyQty, freeQty) =>
if (item.quantity >= buyQty) item.price * freeQty else Money.zero
case FreeShippingOver(threshold) =>
Money.zero
}
There are no strings to compare. No instanceof. No unreachable states. No forgotten cases. In fact, if you add a new promotion type and forget to update this match expression, Scala will refuse to compile until you handle it. That is what correctness by construction looks like.
Pattern matching won’t magically solve eligibility, exclusivity, double-counting qualifiers or stacking logic by itself. What it does is give you a clean, enforceable way to: model the kinds of rules you support, model the policy for how they interact, and make illegal combinations hard (or impossible) to express.
Pattern matching (with sealed ADTs) lets you model promotion shapes and promotion policies explicitly, which is the prerequisite for reliably implementing stacking, eligibility, and allocation without hidden corner cases.
Why This Matters in Real Systems
Pattern matching does more than reorganize the code. It fundamentally changes how we think about domain modeling:
Illegal states become impossible - You cannot construct a PercentageOff without a percentage. You cannot construct a BuyXGetY without the required fields. The type system enforces integrity.
Domain variation becomes explicit - Instead of documenting in a wiki that “a rule may be this or that,” the compiler knows, and enforces, all possible shapes. Business logic becomes easier to read because it mirrors the domain vocabulary.
Exhaustiveness eliminates missing logic - If you forget a case, Scala tells you before your customers do. It is the difference between correctly applying incentives, and silently eroding margin for weeks.
Refactoring becomes safe - When the domain changes, the compiler guides all dependent code to adapt. This is the opposite of Java’s dynamic branching, where new types often break behavior silently.
Pattern matching prepares the mind for Option, Either, and for-comprehensions - This is the conceptual pivot point of the series. Once a developer sees branching as matching on shape, they are ready to understand:
Option as “a value or no value”
Either as “success or failure”
for-comprehensions as “a sequence of dependent matches”
Pattern matching is the entry point for all of these.
A Gentle Warning for Java Developers: Don’t Bring instanceof With You
Scala technically allows:
if (rule.isInstanceOf[PercentageOff]) ...
But that is the wrong instinct. It drags imperative habits forward into functional contexts, and it undermines the entire point of ADTs: to encode variation in types, not in runtime checks. Pattern matching isn’t just prettier — it is structural. It expresses the shape of the domain directly and enforces correctness.
Pattern Matching in Commerce: A Natural Fit
Digital commerce problems often depend on what something is, not just what its values are:
Is this a simple SKU or a kit?
Is this shipment split, consolidated, partial, drop-shipped, or in-store pickup?
Is this return a refund, exchange, or store credit?
Is this tender card-based, gift-card, COD, or split?
Imperative systems bury these distinctions in flags, enumerated types, inheritance, and/or deeply nested conditionals. Pattern matching makes the domain readable. You see the shape, the rule, and the logic in one place — and nothing is hidden behind machinery.