Java Interfaces, Functional Interfaces, Lambdas, Generics and Type Checking Explained with a Real-World Payment System

Learn normal interfaces, marker interfaces, functional interfaces, lambda expressions, generics, instanceof type checking, and event handlers using a realistic payment-event processing system.

Interfaces and Functional Interfaces in Java

Java interfaces define contracts that classes or lambda expressions can follow. A normal interface can contain multiple abstract methods, while a functional interface contains exactly one abstract method and can be implemented using a lambda expression.

Normal Interface

A normal interface is useful when a service needs to define multiple related operations.

interface PaymentService {

    void processPayment(double amount);

    void refundPayment(String transactionId, double amount);

    String getPaymentStatus(String transactionId);
}

PaymentService has three abstract methods, so it is not a functional interface. A class must implement its required methods.

class BankPaymentService implements PaymentService {

    @Override
    public void processPayment(double amount) {
        System.out.println("Processing payment of ₹" + amount);
    }

    @Override
    public void refundPayment(String transactionId, double amount) {
        System.out.println(
            "Refunding ₹" + amount +
            " for transaction " + transactionId
        );
    }

    @Override
    public String getPaymentStatus(String transactionId) {
        return "SUCCESS";
    }
}

Creating an Object Through an Interface Reference

PaymentService paymentService = new BankPaymentService();

paymentService.processPayment(5000.1);

The interface does not create the object. The object is created by new BankPaymentService(). The variable paymentService is an interface reference that points to the BankPaymentService object.

Marker Interface

A marker interface is an interface with no methods. Its purpose is to mark or classify a class as having a particular property.

interface CriticalEvent {
    // No methods
}

Any event that implements CriticalEvent is identified as a critical event.

class PaymentFailedEvent implements CriticalEvent {
    // Payment failure information
}

class AccountLockedEvent implements CriticalEvent {
    // Account locking information
}

Functional Interface

A functional interface has exactly one abstract method. Because it represents one operation, Java allows it to be implemented using a lambda expression.

@FunctionalInterface
interface EventHandler<T extends CriticalEvent> {

    void handle(T event);
}

The generic type T represents the type of event that the handler processes. The T extends CriticalEvent restriction ensures that only critical events can be used with this handler.

Lambda Expression

A lambda provides the implementation of the functional interface's single abstract method without requiring a separate implementation class.

EventHandler<PaymentFailedEvent> handler =
    event -> System.out.println(
        "Payment failed: " + event.getTransactionId()
    );

The lambda parameter event represents the PaymentFailedEvent because the variable is declared as EventHandler<PaymentFailedEvent>.

Lambda with Multiple Statements

When a lambda needs multiple statements, use curly braces.

EventHandler<PaymentFailedEvent> handler =
    event -> {
        System.out.println("Payment failure detected");
        System.out.println("Transaction: " + event.getTransactionId());
        System.out.println("Amount: ₹" + event.getAmount());
    };

Real-World Payment Failure Event

class PaymentFailedEvent implements CriticalEvent {

    private final String transactionId;
    private final String customerId;
    private final double amount;
    private final String reason;

    public PaymentFailedEvent(
            String transactionId,
            String customerId,
            double amount,
            String reason) {

        this.transactionId = transactionId;
        this.customerId = customerId;
        this.amount = amount;
        this.reason = reason;
    }

    public String getTransactionId() {
        return transactionId;
    }

    public String getCustomerId() {
        return customerId;
    }

    public double getAmount() {
        return amount;
    }

    public String getReason() {
        return reason;
    }
}

Creating the Payment Event

PaymentFailedEvent event =
    new PaymentFailedEvent(
        "TXN-98231",
        "CUST-501",
        75000,
        "Bank declined transaction"
    );

Multiple Functional Handlers

Instead of putting customer notifications, operations alerts, audit logging, and other behavior directly inside EventProcessor, each behavior can be supplied as a separate handler.

EventHandler<PaymentFailedEvent> customerNotificationHandler =
    event -> {
        System.out.println(
            "Sending SMS to customer: " +
            event.getCustomerId()
        );

        System.out.println(
            "Payment of ₹" +
            event.getAmount() +
            " failed."
        );
    };

EventHandler<PaymentFailedEvent> operationsHandler =
    event -> {
        System.out.println("ALERT OPERATIONS TEAM");
        System.out.println(
            "Transaction: " +
            event.getTransactionId()
        );
        System.out.println(
            "Reason: " +
            event.getReason()
        );
    };

EventHandler<PaymentFailedEvent> auditLogger =
    event -> {
        System.out.println(
            "[AUDIT] Payment failure recorded: " +
            event.getTransactionId()
        );
    };

Event Bus with Generic Handlers

import java.util.ArrayList;
import java.util.List;

class EventBus<T extends CriticalEvent> {

    private final List<EventHandler<T>> handlers =
        new ArrayList<>();

    public void register(EventHandler<T> handler) {
        handlers.add(handler);
    }

    public void publish(T event) {
        for (EventHandler<T> handler : handlers) {
            handler.handle(event);
        }
    }
}

The EventBus stores multiple handlers and sends an event to every registered handler. This allows one payment failure to trigger multiple independent actions.

Registering Multiple Handlers

EventBus<PaymentFailedEvent> paymentEventBus =
    new EventBus<>();

paymentEventBus.register(customerNotificationHandler);
paymentEventBus.register(operationsHandler);
paymentEventBus.register(auditLogger);

paymentEventBus.publish(event);

Publishing one PaymentFailedEvent now executes all three registered handlers: customer notification, operations alert, and audit logging.

Compile-Time Type Checking with Generics

EventHandler<PaymentFailedEvent> can handle only PaymentFailedEvent objects. Java prevents an AccountLockedEvent from being passed to that handler at compile time.

EventHandler<PaymentFailedEvent> paymentHandler =
    event -> System.out.println(
        event.getTransactionId()
    );

paymentHandler.handle(paymentFailedEvent); // Valid

// paymentHandler.handle(accountLockedEvent); // Compile-time error

Runtime Type Checking with instanceof

class EventProcessor {

    public void process(CriticalEvent event) {

        if (event instanceof PaymentFailedEvent paymentEvent) {

            System.out.println("Payment failure detected");
            System.out.println(
                "Transaction: " +
                paymentEvent.getTransactionId()
            );
            System.out.println(
                "Amount: ₹" +
                paymentEvent.getAmount()
            );

        } else if (event instanceof AccountLockedEvent accountEvent) {

            System.out.println("Account locked");
            System.out.println(
                "Customer: " +
                accountEvent.getCustomerId()
            );

        } else {

            System.out.println("Unknown critical event");
        }
    }
}

instanceof checks the actual object type at runtime. Pattern matching allows Java to both check the type and create a correctly typed variable such as paymentEvent or accountEvent.

Account Locked Event

class AccountLockedEvent implements CriticalEvent {

    private final String customerId;
    private final String reason;

    public AccountLockedEvent(
            String customerId,
            String reason) {

        this.customerId = customerId;
        this.reason = reason;
    }

    public String getCustomerId() {
        return customerId;
    }

    public String getReason() {
        return reason;
    }
}

Different Event Type with Its Own Handlers

AccountLockedEvent accountEvent =
    new AccountLockedEvent(
        "CUST-501",
        "Too many failed login attempts"
    );

EventBus<AccountLockedEvent> accountEventBus =
    new EventBus<>();

accountEventBus.register(
    event -> {
        System.out.println(
            "Account locked: " +
            event.getCustomerId()
        );

        System.out.println(
            "Reason: " +
            event.getReason()
        );
    }
);

accountEventBus.register(
    event -> System.out.println(
        "Sending security email to " +
        event.getCustomerId()
    )
);

accountEventBus.publish(accountEvent);

Complete Main Example

public class Main {

    public static void main(String[] args) {

        PaymentService paymentService =
            new BankPaymentService();

        paymentService.processPayment(5000.1);

        PaymentFailedEvent paymentEvent =
            new PaymentFailedEvent(
                "TXN-98231",
                "CUST-501",
                75000,
                "Bank declined transaction"
            );

        EventHandler<PaymentFailedEvent> customerHandler =
            event -> System.out.println(
                "SMS sent to " +
                event.getCustomerId()
            );

        EventHandler<PaymentFailedEvent> operationsHandler =
            event -> System.out.println(
                "Operations alerted for " +
                event.getTransactionId()
            );

        EventHandler<PaymentFailedEvent> auditHandler =
            event -> System.out.println(
                "Audit log created for " +
                event.getTransactionId()
            );

        EventBus<PaymentFailedEvent> bus =
            new EventBus<>();

        bus.register(customerHandler);
        bus.register(operationsHandler);
        bus.register(auditHandler);

        bus.publish(paymentEvent);
    }
}

How the Architecture Works

  • CriticalEvent marks which event types are considered critical.
  • PaymentService defines a normal multi-method payment contract.
  • EventHandler<T> is a functional interface with one abstract method.
  • The lambda provides the actual behavior of EventHandler.
  • Generics ensure that a handler receives the correct event type.
  • instanceof performs runtime type checking when working with the common CriticalEvent type.
  • EventBus stores multiple handlers and executes them when an event is published.
  • One PaymentFailedEvent can therefore trigger SMS notification, operations alerts, audit logging, and other independent behaviors.

Normal Interface vs Functional Interface vs Marker Interface

  • Normal interface → defines a contract with potentially multiple abstract methods.
  • Functional interface → defines exactly one abstract operation and can be implemented with a lambda.
  • Marker interface → contains no methods and is used to classify or mark a type.
  • Lambda → provides the implementation of a functional interface.
  • Generic type → connects a handler to the specific event type it should process.

Real-World Flow

A payment system detects a failed transaction and creates a PaymentFailedEvent. The event is recognized as a CriticalEvent. The EventBus receives the event and invokes every registered EventHandler<PaymentFailedEvent>. One handler can notify the customer, another can alert operations, and another can write an audit record. The payment processor does not need to know the implementation details of any of these actions.

Interview Questions

  • Can we create an object directly from an interface?
  • Why can a lambda be assigned to a functional interface?
  • What makes an interface a functional interface?
  • What is the purpose of @FunctionalInterface?
  • What is a marker interface?
  • Why is CriticalEvent useful in this example?
  • How do generics provide compile-time type safety?
  • What is the difference between compile-time and runtime type checking?
  • Why is instanceof used when the reference type is CriticalEvent?
  • Why use EventHandler instead of hard-coding all behavior inside EventProcessor?
  • How can one event trigger multiple handlers?
  • What is the difference between an interface reference and the actual object?