Real World Example

Razorpay-style Payment Methods at Checkout

Indian checkouts (Razorpay, PhonePe, Paytm) support cards, UPI, net banking, wallets and cash on delivery. The order needs one Payment reference whatever the method, and finance reports sum all payments. ShopEase uses SINGLE_TABLE plus polymorphic behaviour in each subclass.

@Entity
@Table(name = "payments")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type")
public abstract class Payment extends BaseEntity {
    @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id", unique = true)
    private Order order;
    private BigDecimal amount;
    @Enumerated(EnumType.STRING) private PaymentStatus status = PaymentStatus.PENDING;

    public abstract String receiptLine();
}

@Entity @DiscriminatorValue("UPI")
public class UpiPayment extends Payment {
    private String upiId;
    private String utr;                                   // bank reference number
    public String receiptLine() { return "UPI " + upiId + " (UTR " + utr + ")"; }
}

@Entity @DiscriminatorValue("CARD")
public class CardPayment extends Payment {
    private String network; private String cardLast4;
    public String receiptLine() { return network + " card ending " + cardLast4; }
}

public interface PaymentRepository extends JpaRepository<Payment, Long> {

    @Query("""
           select type(p) as method, count(p) as count, sum(p.amount) as total
           from Payment p
           where p.status = com.shopease.payment.PaymentStatus.CAPTURED
             and p.createdAt >= :from and p.createdAt < :to
           group by type(p)
           """)
    List<MethodTotal> dailyTotals(@Param("from") Instant from, @Param("to") Instant to);
}

Daily Settlement Report

Example

type(p) in JPQL becomes the discriminator column in SQL:

curl "localhost:8080/api/admin/reports/payments?date=2026-03-14"

Example output

DEBUG org.hibernate.SQL : select p1_0.payment_type,count(p1_0.id),sum(p1_0.amount) from payments p1_0 where p1_0.status=? and p1_0.created_at>=? and p1_0.created_at<? group by p1_0.payment_type

HTTP/1.1 200 OK
[
  {"method": "UpiPayment",  "count": 18420, "total": 9214350.00},
  {"method": "CardPayment", "count": 6115,  "total": 7810220.00},
  {"method": "CodPayment",  "count": 2290,  "total": 1544890.00}
]
  • One table, one index on (status, created_at): the report and the order page both stay fast.
  • Adding net banking later = one new subclass with a new discriminator value and one nullable column.
  • Business logic lives in the subclasses (receiptLine()), so services never need instanceof chains.