SwadhyAI Notes

All Low-Level Design notes

Extra pattern · not GoF

Policy object

A policy object holds a domain rule that can change independently of the entity that uses it — refund windows, overtime fees, grace periods. The entity asks the policy; it does not bury the rule in nested ifs.

Example: a refund is “full if you leave within fifteen minutes, half if weather closed the lot, none after payment.” Today that rule is buried in Ticket.exit() as nested ifs. Next week the window is ten minutes. Extract a RefundPolicy so the change is one class.

Where does this rule live when it is not “the algorithm,” just “the rule”?

Buried in the entity

if (minutes < 15) return fee;
if (weatherClosed) return half;
return zero;

Looks small until weather refunds and staff overrides arrive.

Named rule

ticket.refund(policy, clock)

The lives in GracePeriodRefund. Ticket asks; it does not know 15.

The rule has a name

java
interface RefundPolicy {
  Money refundFor(Ticket ticket, Instant now);
}

class GracePeriodRefund implements RefundPolicy {
  private final Duration grace;
  GracePeriodRefund(Duration grace) { this.grace = grace; }

  public Money refundFor(Ticket ticket, Instant now) {
    if (ticket.duration(now).compareTo(grace) <= 0) return ticket.fee();
    return Money.zero(ticket.fee().currency());
  }
}

class Ticket {
  Money refund(RefundPolicy policy, Clock clock) {
    return policy.refundFor(this, clock.instant());
  }
}

Policy and Strategy are cousins

Strategy

How we choose or compute, and we will swap implementations. Hourly vs airport. See Strategy.

Policy

This is the current business rule. Often one class: BookingWindow, CancellationPolicy, OvertimeFee. A , different emphasis.

Specification

Rules you compose with and/or/not. Use Specification when the interviewer will combine constraints.

Keep the policy a pure decision

A takes values in and returns a decision. Freeze the clock, feed a ticket, assert the money. Inject the policy through the constructor — dependency injection — so tests pass a generous grace period and production passes fifteen minutes.

Names that earn their keep

GracePeriodRefund / CancellationPolicy

Time windows and money. The entity asks; it does not store the number 15. Product can change the window without opening Ticket.

BookingWindow

Can this slot be booked at this instant? Calendar and movie ticket rounds grow this rule. Keep it out of the controller.

OvertimeFee

Parking and library late fees. Not “the pricing algorithm” — a threshold on top of the base fee. Pair with Strategy for the base, Policy for the extra.

Burying the rule in the entity

if (minutes < 15) inside Ticket.exit() looks small until weather refunds and staff overrides arrive. Extract the policy while the if is still small.

Policy that writes to the database

A RefundPolicy that also updates a ledger is two jobs. The policy decides. A service or repository applies. Mixing them makes the rule untestable without JDBC.

Protected Variations, said as a product word

Protected Variations says: find the unstable part, hide it behind a stable interface. The unstable part here is the refund window. RefundPolicy is the interface. You do not need to say “I will use Strategy” if the honest name is the policy. Interviewers care that the rule can change without editing Ticket.

Name the rule, not the pattern

Say “RefundPolicy with a fifteen-minute grace.” Do not say “I will use Strategy” unless you will swap implementations in the follow-up. Honest names beat catalog names.

Library overdue fee is a policy

Copy.returnOn(day) should not know “two rupees per day after seven days.” OverduePolicy does. The copy asks. Tests freeze the calendar. That is the same extract as GracePeriodRefund, in a different product.

java
interface OverduePolicy {
  Money feeFor(Loan loan, LocalDate today);
}

class PerDayAfterGrace implements OverduePolicy {
  private final int graceDays;
  private final Money perDay;
  public Money feeFor(Loan loan, LocalDate today) {
    long late = ChronoUnit.DAYS.between(loan.dueOn(), today) - graceDays;
    if (late <= 0) return Money.zero(perDay.currency());
    return perDay.times(late);
  }
}

Weather refund as a second policy

Do not grow GracePeriodRefund with a weather boolean. Compose at the use-case edge: try grace, then weather, then none. Or inject a CompositeRefund that asks a list. Ticket still only knows refund(policy, clock).

java
class FirstMatchRefund implements RefundPolicy {
  private final List<RefundPolicy> rules;
  FirstMatchRefund(List<RefundPolicy> rules) { this.rules = List.copyOf(rules); }

  public Money refundFor(Ticket ticket, Instant now) {
    for (RefundPolicy rule : rules) {
      Money amount = rule.refundFor(ticket, now);
      if (!amount.isZero()) return amount;
    }
    return Money.zero(ticket.fee().currency());
  }
}

That list is still policies, not a specification. Specs filter candidates. Policies decide money. Keep the verbs different so the whiteboard stays readable.

What to write first

Extract the interface and one implementation while the if is still small. Inject Duration grace so the number 15 is not a magic literal in Ticket. That is enough for a parking-lot follow-up. A full rule engine can wait for the rule engine case study.

Related pages