Java · Software Development · Software Engineering
The Ifless Principle: Designing APIs Without Hidden Decisions
Introduction
One of the most dangerous lines of code isn’t the one that throws an exception — it’s the one that hides a decision.
As engineers, we often celebrate flexible methods: a single save() that “magically” knows when to insert or update, a send() that picks the right channel, or an approveOrReject() that decides the outcome. At first, it feels convenient. But hidden behind that convenience lives complexity, ambiguity, and a growing army of if-statements.
The problem with too many ifs is not only code readability. It’s that each of the multiple scenarios your team must design, test, and maintain. What looks like “just one more condition” can easily become exponential in cost.
Over time, I’ve adopted a design approach I call the ifless principle: instead of burying decisions in code, we make them explicit in the design of the API and in the domain itself.
The Ifless Principle
The idea is simple:
- Don’t let your service decide what the user already knows.
- Express different operations as different methods, commands, or entities, even if they initially share behavior.
- Move intelligence into rich domain entities (following Domain-Driven Design), so that rules live where they belong.
In short: ifless is not about eliminating ifs, but about putting decisions in the right place.
Example 1: Save vs Insert/Update
❌ If-full version
public void save(Order order)
{
if (order.getId() == null)
{
insert(order);
}
else
{
update(order);
}
}
✅ Ifless version
public void insert(Order order)
{
// Insert the order.
}
public void update(Order order)
{
// Update the order.
}
Even if both methods are identical today, the design already anticipates that inserts and updates will evolve differently. More importantly, the caller knows the intent, so the service doesn’t have to guess.
Example 2: Approvals
❌ Decision hidden in the service
public void approveOrReject(Transaction transaction, boolean approve)
{
if (approve)
{
// Approve the transaction.
}
else
{
// Reject the transaction.
}
}
✅ Ifless API
public void approve(Transaction transaction)
{
// Approve the transaction.
}
public void reject(Transaction transaction)
{
// Reject the transaction.
}
Each operation has its own lifecycle, rules, and evolution. Tests become more focused, and the API expresses intention clearly.
Example 3: Notifications
❌ Conditional channel selection
notificationService.send(user, message, channel);
✅ Ifless separation
emailNotification.send(user, message);
smsNotification.send(user, message);
pushNotification.send(user, message);
Instead of one method with multiple branching conditions, each channel implements its own rules. Adding a new channel doesn’t mean touching a giant switch-case.
Connection with Domain-Driven Design
In Domain-Driven Design (DDD), the domain model encapsulates the core logic. That means invariants (like “an order can only ship if it is paid”) should live inside the entity itself:
public class Order
{
public void ship()
{
if (!this.isPaid())
{
throw new BusinessException("Order must be paid before shipping");
}
// proceed with shipping...
}
}
Notice: there’s still an if — but it’s not scattered across services. It’s encapsulated in the place where the rule truly belongs: the Order entity.
This is ifless in spirit: decisions are modeled explicitly in the domain, not left to a god-service to decide.
Benefits of the Ifless Principle
- Clarity of API: the method name tells you exactly what it does. No hidden branching.
- Reduced test explosion: each
ifdoubles the number of possible execution paths. Removing branches from services simplifies test design. For example,save()with insert/update needs at least two scenarios; explicitinsert()andupdate()operations each have a narrower contract. - Evolution without risk: as requirements diverge, methods evolve independently. You don’t risk breaking insert logic while changing update.
- Alignment with SRP (Single Responsibility Principle): one method has one reason to change.
- Cleaner architecture: services stay thin, entities stay rich, and decisions are explicit.
Trade-offs and Counterpoints
No principle is universal. Ifless comes with its own costs:
- Verbosity: You might end up with more methods or services, even when differences are minimal.
- Client Burden: Sometimes, callers just want a convenient
save(). Exposing too much detail can reduce ergonomics. - Breaking conventions: Frameworks like Hibernate and Spring Data already assume methods like
save()ormerge(). Going against the grain might surprise new developers.
That’s why I see ifless not as a dogma, but as a compass. Use it when clarity, testability, and explicit design outweigh the convenience of a single method.
Related Ideas and References
- Eric Evans – Domain-Driven Design
- Martin Fowler – Refactoring: Replace Conditional with Polymorphism
- Clean Code by Robert C. Martin
- The “UPSERT” debate in SQL — convenience vs explicitness.
Conclusion
Every if has a cost. Not just in code complexity, but in testing, maintenance, and evolution.
The ifless principle is about making decisions explicit in the design of your API and your domain. It’s about contracts that express intent without ambiguity.
It doesn’t mean you’ll never write an if. It means your architecture won’t hide them in the wrong places.
In the age of scaling startups with fewer resources, clarity in design is not a luxury — it’s survival.
Practical experiment
Find the hidden decision
Read each contract and identify where a choice is already known. Your answers stay in this browser.
What did this article spark?
Choose more than one and undo at any time.
Comments
Loading comments…