Decorator pattern dynamically changes the functionality of an object at run-time without creating new object rather by adding behavior and attribute to existing Object.

Why Decorator Pattern?

It allows adding new functionality to an existing object without altering its original class. This pattern involves wrapping the original object in a decorator class, which has the same interface as the object it decorates. The decorator class then adds its behavior to the object during runtime, allowing for increased flexibility and modularity in programming.

When Decorator Pattern?
If you want to add additional functionality to an existing object (i.e. already instantiated class at runtime), as opposed to object’s class and/or subclass then Decorator pattern should be used. It is easy to add functionality to an entire class of objects by subclassing an object’s class, but it is impossible to extend a single object this way. With the Decorator Pattern, you can add functionality to a single object and leave others like it unmodified.

How Decorator Pattern Implemented?

  1. Create a Interface and a concrete base class which implements Interface. This base class has only default constructor
  2. Create Concrete Decorator Classes which implements Interface. The Constructor of Decorator takes BaseDecorator class(vanilla Object) as argument
  3. The Decorator keeps appending the attributes and behavior to this Vanilla Object

IGift.java

public interface IGift {
    public String addGift();
}

ConcreteGift.java

public class ConcreteGift implements IGift{

    @Override
    public String addGift() {
        return "Gift Item to Cherish";
    }
}

GiftFlowerDecorator.java

public class GiftFlowerDecorator implements IGift{
    private IGift gift;

    public GiftFlowerDecorator(IGift gift) {
        this.gift = gift;
    }

    @Override
    public String addGift() {
        if(this.gift != null){
            return this.gift.addGift() + " with flowers";
        }

        return null;
    }
}

GiftMessageDecorator.java

public class GiftMessageDecorator implements IGift{
    private String message;
    private IGift gift;

    public GiftMessageDecorator(IGift gift){
        this.gift = gift;
    }

    public void setMessage(String message){
        this.message = message;
    }

    @Override
    public String addGift() {
        if (this.gift != null) {
            return this.gift.addGift() + this.message;
        }
        return null;
    }
}

GiftWrapperDecorator.java

public class GiftWrapperDecorator implements IGift{
    private IGift gift;

    public GiftWrapperDecorator(IGift gift) {
        this.gift = gift;
    }

    @Override
    public String addGift() {
        if(this.gift != null){
            return this.gift.addGift() + " and fully Wrapped";
        }

        return null;
    }
}

BuyGiftOnline.java

public class BuyGiftOnline {
    public static void main(String[] args) {
        IGift objGift = new ConcreteGift();
        System.out.println(objGift.addGift());

        GiftMessageDecorator objGiftMsgDec = new GiftMessageDecorator(objGift);
        objGiftMsgDec.setMessage(" with Message");
        System.out.println(objGiftMsgDec.addGift());

        GiftWrapperDecorator objWrapDec = new GiftWrapperDecorator(objGiftMsgDec);
        System.out.println(objWrapDec.addGift());

        GiftFlowerDecorator objFlowerDec = new GiftFlowerDecorator(objWrapDec);
        System.out.println(objFlowerDec.addGift());
    }
}

Output

Gift Item to Cherish
Gift Item to Cherish with Message
Gift Item to Cherish with Message and fully Wrapped
Gift Item to Cherish with Message and fully Wrapped with flowers

Below we have one more example of Decorator Pattern

ILogger.java

public interface ILogger {
    String log(String msg);
}

BasicConcreteLogger.java

public class BasicConcreteLogger implements ILogger{
    public BasicConcreteLogger() {
    }

    @Override
    public String log(String msg) {
        System.out.println(msg);
        return msg;
    }
}

LoggerWithTimeStampDecorator.java

public class LoggerWithTimeStampDecorator implements ILogger{
    ILogger logger;

    public LoggerWithTimeStampDecorator(ILogger logger) {
        this.logger = logger;
    }

    @Override
    public String log(String msg) {
        msg = msg + " TimeStamp:-"+ new java.util.Date();
        System.out.println(msg);
        return msg;
    }
}

LoggerWithUUIDDecorator.java

public class LoggerWithUUIDDecorator implements ILogger{
    ILogger logger;
    public LoggerWithUUIDDecorator(ILogger logger) {
        this.logger = logger;
    }

    @Override
    public String log(String msg) {
        msg = msg + " UUID:- "+ UUID.randomUUID().toString();
        System.out.println(msg);
        return msg;
    }
}

LoggerUtil.java

public class LoggerUtil {
    public static void main(String[] args) {
        BasicConcreteLogger objLogger = new BasicConcreteLogger();
        String msg = objLogger.log("Sample Log Message");

        LoggerWithTimeStampDecorator objLTS = new LoggerWithTimeStampDecorator(objLogger);
        msg = objLTS.log(msg);

        LoggerWithUUIDDecorator objUUID = new LoggerWithUUIDDecorator(objLogger);
        objUUID.log(msg);
    }
}

Output

Sample Log Message
Sample Log Message TimeStamp:-Sat Nov 09 13:44:31 IST 2024
Sample Log Message TimeStamp:-Sat Nov 09 13:44:31 IST 2024 UUID:- deacc0f1-8817-41c3-97d5-e05dd9909b57

Strategy Design Pattern provides a way to extract the behavior of an object into separate classes that can be swapped in and out at runtime

Why Strategy Design Pattern
This pattern enables an object to choose from multiple algorithms and behaviors at runtime, rather than statically choosing a single one.

PaymentProcessor.java

public class PaymentProcessor{
    private PaymentType paymentType;

    public void processPayment(String paymentAmount){
        if(paymentType.equals(PaymentType.CASH)){
            System.out.println("Cash Payment on Delivery");
        }else if(paymentType.equals(PaymentType.NET_BANKING)){
            System.out.println("Net Banking Payment ");
        }else if(paymentType.equals(PaymentType.UPI)){
            System.out.println("UPI Payment by GPay or Paytm ");
        }else if(paymentType.equals(PaymentType.DEBIT_CARD)){
            System.out.println("Debit Card Payment ");
        }else if(paymentType.equals(PaymentType.CREDIT_CARD)){
            System.out.println("Credit Card Payment ");
        }else{
            System.out.println("Gift Coupon");
        }

    }

    public void setPaymentType(PaymentType paymentType) {
        this.paymentType = paymentType;
    }
}

Problem with above code
In this code, the PaymentProcessorclass has a processPayment method that takes a payment amount and processes the payment. The payment type is set using the setPaymentType method, which sets the paymentType field. The processPayment method then checks the value of paymentType and processes the payment accordingly.

The problem with this code is that it violates the Open-Closed Principle, which states that classes should be open for extension but closed for modification. In this code, if you want to add a new payment type, you would have to modify the processPayment method, which violates the Open-Closed Principle.

When Strategy Design Pattern
Places where different algorithms(Strategy) is used to address a single solution I.E Different sorting algorithms can be encapsulated into separate strategies and passed to an object that needs sorting, Different formatting strategies can be encapsulated into separate strategies and passed to an object that needs formatting, Different payment methods can be encapsulated into separate strategies and passed to an object that needs to process payments.

How Strategy Design Pattern
The Strategy Design Pattern works by separating the behavior of an object from the object itself. The behavior is encapsulated into different strategies, each with its own implementation of the behavior.The context maintains a reference to a strategy object and interacts with it through a common interface. At runtime, the context can swap the current strategy with another one, effectively changing the object’s behavior.

  1. Identify the algorithm or behavior that needs to be encapsulated and made interchangeable.
  2. Define an interface that represents the behavior, with a single method signature that takes in any required parameters.
  3. Implement concrete classes that provide specific implementations of the behavior defined in the interface.
  4. Define a context class that holds a reference to the interface and calls its method when needed.
  5. Modify the context class to allow for the dynamic swapping of the concrete implementations at runtime.

OfferStrategy.java

public interface OfferStrategy {
    public Integer getOfferDiscount();
}

CashPaymentStrategy.java

public class CashPaymentStrategy implements OfferStrategy {
    @Override
    public Integer getOfferDiscount() {
        return 0;
    }
}

DebitCardPaymentStrategy.java

public class DebitCardPaymentStrategy implements OfferStrategy {
    @Override

    public Integer getOfferDiscount() {
        return 5;
    }
}

UPIPaymentStrategy.java

public class UPIPaymentStrategy implements OfferStrategy {
    @Override
    public Integer getOfferDiscount() {
        return 3;
    }
}

OfferStrategyContext.java

public class OfferStrategyContext {
    public Integer totalBillAmount;

    static Map<PaymentType, OfferStrategy>  strategyContext = new HashMap<PaymentType, OfferStrategy>();

    static{
        strategyContext.put(PaymentType.DEBIT_CARD, new DebitCardPaymentStrategy());
        strategyContext.put(PaymentType.CASH, new CashPaymentStrategy());
        strategyContext.put(PaymentType.UPI, new UPIPaymentStrategy());
    }

    public void applyStrategy(PaymentType paymentType) {
        OfferStrategy offerStrategy = strategyContext.get(paymentType);
        Double discAmt =  totalBillAmount*(offerStrategy.getOfferDiscount()*0.01);
        Double finalPrice = (totalBillAmount - discAmt);

        System.out.println("Discount of " + offerStrategy.getOfferDiscount() + "% applied on Total amount :" + totalBillAmount);
        System.out.println("Discount Amount :"+ discAmt);
        System.out.println("Final Amount to be paid after offer:" + finalPrice);
    }

    public OfferStrategyContext(Integer totalBillAmount) {
        this.totalBillAmount = totalBillAmount;
    }
}

PaymentType.java

public enum PaymentType {
    CREDIT_CARD("CREDIT_CARD"), DEBIT_CARD("DEBIT_CARD"), CASH("CASH"), COUPON("COUPON"),UPI("UPI");

    private String paymentMode;

    PaymentType(String paymentMode) {
        this.paymentMode = paymentMode;
    }
}

PlaceOrder.java

public class PlaceOrder {
    public Integer amount;

    public static void main(String[] args) {
        OfferStrategyContext objOfferStrCtxt = new OfferStrategyContext(100);
        objOfferStrCtxt.applyStrategy(PaymentType.DEBIT_CARD);
    }
}

Output

Discount of 5% applied on Total amount :100
Discount Amount :5.0
Final Amount to be paid after offer:95.0

Note:
The same strategy design pattern could be implemented using Factory method by moving code to Factory class and deciding the strategy based on the static factory method call.

Implementation using Factory Method

PaymentFactory.java

public class PaymentFactory {
    private static CashPayment objCashPayment =  new CashPayment();
    private static DebitCardPayment objDebitCardPayment =  new DebitCardPayment();
    private static CreditCardPayment objCreditCardPayment =  new CreditCardPayment();
    private static UPIPayment objUPIPayment =  new UPIPayment();
    private static CouponPayment objCouponPayment =  new CouponPayment();

    public static PaymentStrategy  payementStrategyFactory(PaymentType paymentType){
        if(paymentType.equals(PaymentType.CASH)){
            return objCashPayment;
        }else if(paymentType.equals(PaymentType.UPI)){
            return objUPIPayment;
        }else if(paymentType.equals(PaymentType.DEBIT_CARD)){
            return objDebitCardPayment;
        }else if(paymentType.equals(PaymentType.CREDIT_CARD)){
            return objCreditCardPayment;
        }else{
            return objCouponPayment;
        }
    }
}

MakePaymentClient.java

public class MakePaymentClient {
    public static void main(String[] args) {
        PaymentStrategy objPaymentStrategy = PaymentFactory.payementStrategyFactory(PaymentType.CREDIT_CARD);
        objPaymentStrategy.paymentMethod("300");
    }
}  
300 paid in credit card

Reference to Factory Pattern

The Observer Pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically. The observer pattern is a behavioral design pattern that defines a one-to-many relationship between objects. It allows an object, called the subject, to notify other objects, called observers, about any changes in its state. The observers can then react to these changes according to their own logic.

Why Observer Design Pattern
The subject and the observers are loosely coupled, meaning that they do not depend on each other’s implementation details. The subject only knows that it has a list of observers, and the observers only know that they can update themselves when the subject changes.

When Observer Design Pattern
The observer pattern is especially useful in event-driven systems, where the system reacts to events that occur asynchronously. An event is a change in the state of the system or its components, such as a user input, a network request, or a timer. Use the pattern when some objects in your app must observe others, but only for a limited time or in specific cases. Use the Observer pattern when changes to the state of one object may require changing other objects, and the actual set of objects is unknown beforehand or changes dynamically.

How Observer Design Pattern

  1. Declare the subscriber interface(IObserver.java). At a bare minimum, it should declare a single update method.
  2. Declare the publisher interface(Observable.java) and describe a pair of methods for adding a subscriber(Observer) object to and removing it from the list. Remember that publishers must work with subscribers only via the subscriber interface.
  3. Create concrete publisher classes. Each time something important happens inside a publisher, it must notify all its subscribers.
  4. Implement the update notification methods in concrete subscriber classes. Most subscribers would need some context data about the event. It can be passed as an argument of the notification method.

IObserver.java

public interface IObserver {
    public void notify(String serviceName);
}

Observer.java

public class Observer implements IObserver {
    @Override
    public void notify(String serviceName) {
            System.out.println("Notification from " + serviceName);
    }
}

Observable.java

public class Observable {
    private List<IObserver> observers = new ArrayList<IObserver>();

    public void addObserver(IObserver observer){
        this.observers.add(observer);
    }

    public void execute(){
        for (IObserver observer : observers) {
            observer.notify("Email Service");
        }
    }
}

Client.java

public class Client {
    public static void main(String[] args) {
        Observable observable = new Observable();
        observable.addObserver(new Observer());
        observable.execute();
    }
}

IObserver.java

public interface IObserver {
    public void notify(String foodItem);
}

DeliveryOrderObserver.java

public class DeliveryOrderObserver implements IObserver {
    @Override
    public void notify(String foodItem) {
        System.out.println("Ready for Pickup - "+ foodItem);
    }
}

FoodOrderObserver.java

public class FoodOrderObserver implements IObserver {
    @Override
    public void notify(String foodItem) {
        System.out.println(" Order received - "+ foodItem);
    }
}

FoodPickUpObserver.java

public class FoodPickUpObserver implements IObserver {
    @Override
    public void notify(String foodItem) {
        System.out.println("Parcel - "+ foodItem);
    }
}

PrepareFoodObserver.java

public class PrepareFoodObserver implements IObserver {
    @Override
    public void notify(String foodItem) {
        System.out.println("Preparing - "+ foodItem);
    }
}

Observable.java

public class Observable {
    private List<IObserver> observers = new ArrayList<IObserver>();

    public void addObserver(IObserver observer){
        this.observers.add(observer);
    }

    public void execute(){
            for(IObserver observer: observers){
                observer.notify("Egg Biryani");
        }
    }
}

FoodAppClient.java

public class FoodAppClient {
    public static void main(String[] args) {
        Observable observable = new Observable();

        observable.addObserver(new FoodOrderObserver());
        observable.addObserver(new PrepareFoodObserver());
        observable.addObserver(new FoodPickUpObserver());
        observable.addObserver(new DeliveryOrderObserver());
        observable.execute();
    }
}
Order received - Egg Biryani
Preparing - Egg Biryani
Parcel - Egg Biryani
Ready for Pickup - Egg Biryani

Advantages of Observer Pattern

  1. The subject and the observers can be reused in different contexts, as long as they implement the common interface. The subject does not need to know the details of the observers, and the observers do not need to know the details of the subject. The subject and the observers can be added, removed, or changed at runtime, without affecting the rest of the system.
  2. The subject can notify the observers asynchronously, without waiting for their responses. The observers can process the events in parallel, without blocking the subject. This allows for the subject and the observers to adapt to changing requirements and scenarios.

How to Delete duplicate Rows from Table

In the below table studId 1,6 and 9 is repeated which should be deleted.

studId studentName age
1 Mugil 35
2 Vinu 36
3 Viju 42
4 Mani 35
5 Madhu 36
6 Mugil 35
7 Venu 37
8 Banu 34
9 Mugil 35

Below query wont work on MySQL but the format doesn’t change. When you take Max only last occurrence of row would be taken and others would be excluded.

DELETE FROM tblStudents TS
 WHERE TS.studId NOT IN (SELECT MAX(TSS.studId)
                            FROM tblStudents TSS
                        GROUP BY TSS.studentName, TSS.age)c

The same could be done using MIN function.

SELECT TS.* FROM tblStudents TS
 WHERE TS.studId NOT IN (SELECT MIN(TSS.studId)
                           FROM tblStudents TSS
                          GROUP BY TSS.studentName, TSS.age)                        

Output

studId studentName age
6 Mugil 35
9 Mugil 35

Simple Program using Runnable and Callable

HelloThread1.java

public class HelloThread1 implements Runnable{
    @Override
    public void run() {
        System.out.println("Hello World from Thread Name (" + Thread.currentThread().getName() +") using Runnable ");
    }
}

HelloThread2.java

public class HelloThread2 implements Callable {
    @Override
    public Object call() throws Exception {
        return "Hello World from Thread Name (" + Thread.currentThread().getName() +") using Callable";
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws Exception {
        HelloThread1 objThread1 = new HelloThread1(); //Instance of Runnable
        HelloThread2 objThread2 = new HelloThread2(); //Instance of Callable

        objThread1.run();
        System.out.println(objThread2.call());
    }
}

Output

Hello World from Thread Name (main) using Runnable 
Hello World from Thread Name (main) using Callable

Simple Program to print numbers using threads
NumberPrinter.java

public class NumberPrinter implements Runnable{
    int number;
    public NumberPrinter(int number){
        this.number = number;
    }

    public void run(){
        System.out.println("Printing Number from Thread "+ this.number);
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        for (int idx=1;idx<=5;idx++){
            Thread objthread = new Thread(new NumberPrinter(idx));
            objthread.start();
        }
    }
}

Output

Printing Number from Thread 5
Printing Number from Thread 1
Printing Number from Thread 4
Printing Number from Thread 3
Printing Number from Thread 2

Simple Program using Executor Service taking Runnable as Argument

ExecutorService is a framework which allows to create thread. Threads can be created from FixedThreadPool, CachedThreadPool and ScheduledThreadPool. submit() method takes runnable or callable object (Functional Interface Type) as argument. The Same code above can be rewritten as below
Main.java

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService objExecService = Executors.newFixedThreadPool(2);

        //Lambda Expresssion passed as Argument as Runnable is FI
        objExecService.submit(() -> {
            System.out.println(Thread.currentThread().getName());
        });

        objExecService.shutdown();
    }
}

Output

pool-1-thread-1

Same code with Runnable instance passed as argument to submit

  .
  . 
  //Instance of Runnable passed as argument
  HelloThread1 objHT1 = new HelloThread1();
  objExecService.submit(objHT1);
  .
  .

Output

Hello World from Thread Name (pool-1-thread-1) using Runnable 

Same code with Runnable as Anonymous Class passed as argument

ExecutorService exec = Executors.newFixedThreadPool(2);

//Instance of Runnable passed as Anonymous class
exec.execute(new Runnable() {
  public void run() {
    System.out.println("Hello world");
  }
});

exec.shutdown();

Simple Program using Executor Service taking Callable as Argument

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService objExecService = Executors.newFixedThreadPool(2);
        Future<String> objFuture = objExecService.submit(new HelloThread2());
        System.out.println(objFuture.get());
        objExecService.shutdown();
    }
}

Output

Hello World from Thread Name (pool-1-thread-1) using Callable

Using Lambda Expression as Submi

.
.
ExecutorService objExecService = Executors.newFixedThreadPool(2);

Future<String> objFuture = objExecService.submit(() -> {
  Thread.sleep(3000);
  return Thread.currentThread().getName();
});

System.out.println(objFuture.get());
.
.

The above could be rewritten in anonymous class as below

ExecutorService objExecService = Executors.newFixedThreadPool(2);

Future<String> objFuture = objExecService.submit(new Callable<String>() {
 @Override
 public String call() throws Exception {
   Thread.sleep(3000);
   return Thread.currentThread().getName();
 }
});

System.out.println(objFuture.get());
objExecService.shutdown();

Program for Creating Thread Pool and executing Task

ThreadPoolExample.java

public class ThreadPoolExample {
    public static void main(String args[]) {
       ExecutorService service = Executors.newFixedThreadPool(10); //create 10 worker threads in Thread Pool
       for (int i =0; i<100; i++){
           service.submit(new Task(i)); //submit that to be done 
       }
       service.shutdown();
    }  
}

Task.java

final class Task implements Runnable {
    private int taskId;  
    public Task(int id){
        this.taskId = id;
    }
  
    @Override
    public void run() {
        System.out.println("Task ID : " + this.taskId +" performed by " 
                           + Thread.currentThread().getName());
    }  
}
Task ID : 0 performed by pool-1-thread-1
Task ID : 3 performed by pool-1-thread-4
Task ID : 2 performed by pool-1-thread-3
Task ID : 1 performed by pool-1-thread-2
Task ID : 5 performed by pool-1-thread-6
Task ID : 4 performed by pool-1-thread-5
  1. NEW – a newly created thread that has not yet started the execution
  2. RUNNABLE – either running or ready for execution but it’s waiting for resource allocation
  3. BLOCKED – waiting to acquire a monitor lock to enter or re-enter a synchronized block/method
  4. WAITING – waiting for some other thread to perform a particular action without any time limit
  5. TIMED_WAITING – waiting for some other thread to perform a specific action for a specified period
  6. TERMINATED – has completed its execution

NEW Thread (or a Born Thread) is a thread that’s been created but not yet started.
It remains in this state until we start it using the start() method

NewState.java

public class NewState implements Runnable{
    public void run(){
        System.out.println("I am in new State");
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread objThread = new Thread(new NewState());
       System.out.println(objThread.getState());
    }
}

Output

NEW

Runnable When we’ve created a new thread and called the start() method on that, it’s moved from NEW to RUNNABLE state. Threads in this state are either running or ready to run, but
they’re waiting for resource allocation from the system. In a multi-threaded environment, the Thread-Scheduler (which is part of JVM) allocates a fixed amount of time to each thread. So it runs for a particular amount of time, then leaves the control to other RUNNABLE threads.

RunnableState .java

public class RunnableState implements Runnable{
    public void run(){
        System.out.println("I would be in Runnable State");
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread objRThread = new Thread(new RunnableState());
       objRThread.start();
       System.out.println(objRThread.getState());
    }
}

Output

RUNNABLE
I would be in Runnable State

This is the state of a dead thread. It’s in the TERMINATED state when it has either finished execution or was terminated abnormally.
TerminatedState.java

public class TerminatedState implements Runnable{
    public void run(){
        Thread objNewState = new Thread(new NewState());
        objNewState.start();
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread objTState = new Thread(new TerminatedState());
       objTState.start();
       objTState.sleep(1000);
       System.out.println("T1 : "+ objTState.getState());
    }
}

Output

I am in new State
T1 : TERMINATED

A thread is in the BLOCKED state when it’s currently not eligible to run. It enters this state when it is waiting for a monitor lock and is trying to access a section of code that is locked by some other thread.
BlockedState.java

public class BlockedState implements Runnable{
    public void run(){
      blockedResource();
    }

    public static synchronized void blockedResource(){
        while(true){
            //Do Nothing
        }
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread objB1Thread = new Thread(new BlockedState());
        Thread objB2Thread = new Thread(new BlockedState());

        objB1Thread.start();
        objB2Thread.start();

        Thread.sleep(1000);

        System.out.println(objB1Thread.getState());
        System.out.println(objB2Thread.getState());
        System.exit(0);
    }
}

Output

RUNNABLE
BLOCKED

A thread is in WAITING state when it’s waiting for some other thread to perform a particular action. According to JavaDocs, any thread can enter this state by calling any one of the following
object.wait() (or) thread.join() (or) LockSupport.park()

WaitingState.java

public class WaitingState implements Runnable{
    public void run(){
        Thread objWaitState = new Thread(new SleepState());

        objWaitState.start();

        try {
            objWaitState.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

SleepState.java

public class SleepState implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread objWaitingThread = new Thread(new WaitingState());
       objWaitingThread.start();
       objWaitingThread.sleep(1000);
       System.out.println("T1 : "+ objWaitingThread.getState());
       System.out.println("Main : "+Thread.currentThread().getState());
    }
}

Output

T1 : WAITING
Main : RUNNABLE

A thread is in TIMED_WAITING state when it’s waiting for another thread to perform a particular action within a stipulated amount of time. According to JavaDocs, there are five ways to put a thread on TIMED_WAITING state:
thread.sleep(long millis) (or) wait(int timeout) (or) wait(int timeout, int nanos) thread.join(long millis) (or) LockSupport.parkNanos (or) LockSupport.parkUntil

TimedWaitState.java

public class TimedWaitState implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread objTWState = new Thread(new TimedWaitState());
        objTWState.start();
        Thread.sleep(2000);
        System.out.println("T1 : "+ objTWState.getState());
    }
}

Output

T1 : TIMED_WAITING
  1. We use ReentrantLock for locking the Resource(totalSeats)
  2. Incase anything goes wrong (Exception being thrown etc.) you want to make sure the lock is released no matter what.
  3. Calling the reserveSeats method should be done inside separate threads

ReservationSystem.java

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReservationSystem {
    private Integer totalSeats;
    private final Lock lock = new ReentrantLock();

    public ReservationSystem(Integer totalSeats){
        this.totalSeats = totalSeats;
    }

    public Integer getTotalSeats(){
        return totalSeats;
    }

    public void reserveSeats(String userName, int numOfSeats){
        lock.lock();

        try{
            if(numOfSeats >0 && totalSeats>numOfSeats){
                totalSeats -= numOfSeats;
                System.out.println(userName + " has reserved "+ numOfSeats + " with " + totalSeats + " still available");
            }else{
                System.out.println("Seats not Available");
            }
        }finally {
            lock.unlock();
        }
    }
}

BookSeat.java

public class BookSeat {
    public static void main(String[] args) {
        ReservationSystem objResSys = new ReservationSystem(100);

        System.out.println("Total available Seats "+ objResSys.getTotalSeats());

        Thread objThread1 = new Thread(() -> {objResSys.reserveSeats("User1", 10);});
        Thread objThread2 = new Thread(() -> {objResSys.reserveSeats("User2", 20);});
        Thread objThread3 = new Thread(() -> {objResSys.reserveSeats("User3", 5);});


        objThread1.start();
        objThread2.start();
        objThread3.start();

        try {
            objThread1.join();
            objThread2.join();
            objThread3.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("Remaining available Seats "+ objResSys.getTotalSeats());
    }
}


Total available Seats 100
User2 has reserved 20 with 80 still available
User1 has reserved 10 with 70 still available
User3 has reserved 5 with 65 still available
Remaining available Seats 65

Banking System

  1. We have Bank Account with 2 Fields – balance and Account Number
  2. We have Transaction class implementing Runnable
  3. We create object for account with some initial balance and try to pass as parameter to runnable Transaction Object

BankAccount.java

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BankAccount {
    private Integer balance;
    private Integer accountNumber;

    private final Lock reLock = new ReentrantLock();

    public BankAccount(Integer balance, Integer accountNumber){
        this.balance = balance;
        this.accountNumber = accountNumber;
    }

    public void debitAmount(Integer amount){
        reLock.lock();

        try{
            balance -= amount;
        }finally {
            reLock.unlock();
        }

    }

    public void creditAmount(Integer amount){
        reLock.lock();

        try{
            balance += amount;
        }finally {
            reLock.unlock();
        }
    }

    public Integer getAccountNumber(){
        return this.accountNumber;
    }

    public Integer getBalance(){
        return this.balance;
    }

}

BankTransaction.java

public class BankTransaction implements Runnable{
    public Integer transAmount;
    public BankAccount bankAccount;

    public BankTransaction(Integer transAmount, BankAccount bankAccount){
        this.transAmount  = transAmount;
        this.bankAccount  = bankAccount;
    }


    @Override
    public void run() {
        if(transAmount >= 0){
            bankAccount.creditAmount(transAmount);
        }else{
            bankAccount.debitAmount(Math.abs(transAmount));
        }
    }
}

BankSystem.java

public class BankSystem {
    public static void main(String[] args) {
        BankAccount objAcc1 = new BankAccount(1000, 101);
        BankAccount objAcc2 = new BankAccount(2000, 102);

        Thread objThread1 = new Thread(new BankTransaction(50, objAcc1));
        Thread objThread2 = new Thread(new BankTransaction(-150, objAcc2));
        Thread objThread3 = new Thread(new BankTransaction(250, objAcc2));
        Thread objThread4 = new Thread(new BankTransaction(250, objAcc1));

        objThread1.start();
        objThread2.start();
        objThread3.start();
        objThread4.start();

        try{
            objThread1.join();
            objThread2.join();
            objThread3.join();
            objThread4.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("Final Balance in Account " + objAcc1.getAccountNumber() + " with balance " + objAcc1.getBalance());
        System.out.println("Final Balance in Account " + objAcc2.getAccountNumber() + " with balance " + objAcc2.getBalance());
    }
}

Output

Final Balance in Account 101 with balance 1300
Final Balance in Account 102 with balance 2100

Kafka
One of the activity in application is to transfer data from Source System to Target System. Over period of time this communication becomes complex and messy. Kafka provides simplicity to build real-time streaming data pipelines and real-time streaming applications.Kafka Cluster sits in the middle of Source System and Target System. Data from Source System is moved to Cluster by Producer and Data from Cluster is moved from to Target System by Consumer.

        Source System -> {Producer} -> [Kafka Cluster] -> {Consumer} -> Target System

Kafka Provides Seamless integration across applications hosted in multiple platforms by acting as a intermediate.

  1. Sequence of message are called Data Stream. Topic is a particular stream of Data. Topics are organized inside cluster. Topics are like rows in a database which are identified by Topic name.
  2. Cluster Contains Topics -> Topics contains Data Stream -> Data Stream is Made of Seq of Messages
  3. To add(write) Data to Topic, we use Kafka Producer and to read data we use kafka consumers.
  4. Topics
    1. What is Topic? Topics are the categories used to organize messages.Topics are like rows in a table which are identified by Topic name(table name).
    2. Why it is Needed? Logical channel for producers to publish messages and consumers to receive them I.E. Processing payments, Tracking Assets, Monitoring patients, Tracking Customer Interactions
    3. How it works? Logical channel for producers to publish messages and consumers to receive them.A topic is a log of events. Logs are easy to understand, because they are simple data structures with well-known semantics.

  5. Partitions
    1. What are Partitions ? Topics are split into multiple partitions. Messages sent to Topic end up in these partitions, and the messages are ordered by Id(Kafka Partition Offsets). A partition in Kafka is the storage unit that allows for a topic log to be separated into multiple logs and distributed over the Kafka cluster.
      Partitions are immutable. Once data written to partition cannot be changed. Data is kept for one Week which is default configuration.
    2. Why Partition is needed? Partitions allow Kafka to scale horizontally by distributing data across multiple brokers.Multiple consumers can read from different partitions in parallel, and multiple producers can write to different partitions simultaneously. Each partition can have multiple replicas spread across different brokers.
    3. How Partition works? By breaking a single topic log into multiple logs, which are then spread across one or more brokers. This allows Kafka to scale and handle large amounts of data efficiently

  6. Broker

    1. What is Broker? is a server that manages the flow of transactions between producers and consumers in Apache Kafka. Kafka brokers store data in topics, which are divided into partitions. Each broker hosts a set of partitions. Brokers handle requests from clients to write and read events to and from partitions
    2. How Broker works?One broker acts as the Kafka controller (Kafka Broker Leader), which does administrative task, maintaining the state of the other brokers, health check of brokers and reassigning work
    3. Why Broker is required?Producers connect to a broker to write events, while consumers connect to read events.
  7. Offset
    1. What is Offset? Offset is a unique identifier for a message in a Kafka partition. An offset is an integer that represents the position of a message in a partition’s log. The first message in a partition has an offset of 0, the second message has an offset of 1, and so on.
    2. Why it needed? Offsets enable Kafka to provide sequential, ordered, and replayable data processing. This numerical value helps Kafka keep track of progress within a partition. It also allows Kafka to scale horizontally while staying fault-tolerant.
    3. How it works? When a producer publishes a message to a Kafka topic, it’s appended to the end of the partition’s log and assigned a new offset. Consumers maintain their current offset, which indicates the last processed message in each partition.
  8. Producer
    1. What is Producer? Producer writes data to Kafka broker which would be picked by consumer. A producer can send anything, but it’s typically serialized into a byte array. It can also include a message key, timestamp, compression type, and headers.
    2. How Producer works? A producer writes messages to a Kafka broker, which then adds a partition and offset ID to the message.
    3. Why Producer is needed? It allows applications to send streams of data to the Kafka cluster
    4. Producer uses partitioner to decide to which partition the data should write. Producer doesnot decided the broker rather it endup in the respective broker because of the partition presence.
    5. Producer has message keys in message which they send. If the key is null, the data is sent using a round-robin mechanism for writing. If the key is not null, then it would end up in the same partition based on the key. Message ordering is possible with key
  9. Consumer
    1. What is Consumer? Consumer reads data from Kafka broker.
    2. How Consumer works? A consumer issues fetch requests to brokers for partitions it wants to consume. It specifies a log offset, and receives a chunk of log that starts at that offset position. The consumer should know in advance the format of the message.
    3. Why Consumer is needed? It allows applications to receive streams of data from the Kafka broker
  10. Consumer Group
    1. What is Consumer Group? a collection of consumer applications that work together to process data from topics in parallel
    2. How Consumer Group works? A consumer group divides the partitions of a topic among its consumers. Each consumer is assigned a subset of partitions, and only one consumer can process a given partition.
    3. Why Consumer group is needed?allow multiple consumers to work together to process events from a topic in parallel. This is important for scalability, as it enables consumers to read from many events simultaneously.
  11. Messages

    1. What is Message?Kafka messages are created by the producer using serialization mechanism
    2. How Message works? Kafka messages are stored as serialized bytes and have a key-value structure
    3. Why Message is needed? Basic Unit of data in Kafka
    4. Key is a unique identifier of the Partition, which would be null first time. Value is the actual message. Both Key and value would be in Binary format
  12. Partioner

    1. What is Partioner?Kafka’s partitioning feature is a key part of its ability to scale and handle large amounts of data. Partioning is done by partioner
    2. How Partioner works?A Kafka partitioner uses hashing to determine which partition a message should be sent to. It employs Key hashing technique that allows for related messages to be grouped together and processed in the correct order. For example, if a Kafka producer uses a user ID as the key for messages about various users, all messages related to a specific user will be sent to the same partition.
  13. Zookeeper

    1. What is Zookeeper?ZooKeeper is a software tool that helps maintain naming and configuration data, and provides synchronization within distributed systems
    2. How Zookeeper works? Zookeeper keeps track of which brokers are part of the Kafka cluster. Zookeeper is used by Kafka brokers to determine which broker is the leader of a given partition and topic and perform leader elections. Zookeeper stores configurations for topics and permissions. Zookeeper sends notifications to Kafka in case of changes (e.g. new topic, broker dies, broker comes up, delete topics, etc.…)