Showing posts with label JAVA. Show all posts
Showing posts with label JAVA. Show all posts

Saturday, June 28, 2025

Interface Segregation Principle(ISP) in Java

java,programming,software development,technology,design principle
Recently, I came across a large interface with many methods. I wonder why a class need to implement these methods which are not related to it. Fortunately, while searching for a solution, I discovered the Interface Segregation Principle. It is one of five SOLID principles in object-oriented design.
SOLID Design Principles in Java

This blog post reviews the Interface Segregation Principle (ISP) and shows how it can be applied using Java examples.

Interface Segregation Principle

It tells us:
Clients should not be force to depend upon interfaces that they do not use.
It means we shouldn't make our interfaces bigger. We should not pollute our interfaces with irrelevant methods. And force the classes to implement methods they don't care about. Make smaller, more specific interfaces instead of one big one that does everything. To help you understand this better, let's look at the busy world of a restaurant.

Bad Design: Violates ISP

Consider the situation: we are creating software to control how a restaurant works. Perhaps our first thought is to design a single interface for every employee:
Now, let's consider our staff roles: a Chef, a Waiter.

The Chef class implements the IRestaurant interface.
Interface Segregation Principle(ISP) in Java

Waiter class also implements the IRestaurant interface.
Interface Segregation Principle(ISP) in Java
As we can see, both the Chef and Waiter classes implement the IRestaurant interface. But we are forcing the Chef to take on all of the restaurant's responsibilities. The Chef class implements methods such as takeOrder() and washDishes(), which are not part of their job description. This is also true of the Waiter. The Waiter class is also required to include methods such as cookDish() and washDishes(). Here we end up with UnsupportedOperationException or empty method bodies.

It is making our code:
  • Bloated: In our Chef and Waiter class, there are some unnecessary methods.
  • Confusing: It is not immediately clear what a Chef or Waiter does.
  • Fragile: Any change to the IRestaurant interface would cause the Chef and Waiter classes to change, even if it was irrelevant to them.

Small, Specific Interfaces: Now we are on the right path

So, according to the ISP, each interface should be responsible for a specific set of tasks. We must create multiple, smaller interfaces with related responsibilities.

Let's refactor the IRestaurant interface. We built two interfaces, ICook and IWaiter.

So Chef implements the ICook interface.
Interface Segregation Principle(ISP) in Java

And Waiter implements the IWaiter interface.
Interface Segregation Principle(ISP) in Java

The Benefits of ISP

  • Clarity and Readability: Each class now only includes methods that are directly relevant to its role.
  • Reduced Coupling: Our classes are no longer reliant on methods they don't employ. Changes to one interface will not cause unrelated classes to change.
  • Increased Flexibility: If we add a new role, the existing interfaces will not be affected.

The Interface Segregation Principle is an effective tool for creating robust and maintainable Java applications. With these monolithic interfaces, we can create Java applications that are more flexible and easier to understand.

Happy coding!!! 😊
in

Sunday, June 22, 2025

Liskov Substitution Principle (LSP) in Java

java,programming,software development,technology,design principle
The SOLID principles are a set of five design principles that enhances object-oriented programming (OOP). This blog post reviews the Liskov Substitution Principle(LSP) and shows how it can be applied using Java examples.

SOLID Design Principle is made up of five different principles:
  • S: Single Responsibility Principle(SRP)
  • OOpen-Closed Principle(OCP)
  • LLiskov Substitution Principle(LSP)
  • IInterface Segregation Principle(ISP)
  • DDependency Inversion Principle DIP)
SOLID Design Principles in Java

Liskov Substitution Principle

The Liskov Substitution Principle (LSP) emphasizes that objects from a superclass can be replaced with objects from a subclass without affecting the program's correctness. This principle ensures that derived classes follow the contract established by their base classes, thereby improving code reliability and preventing unexpected behavior in polymorphic scenarios.

This is the third principle of the SOLID design principle group. It tells us:
We can replace subclass or child class objects with base class objects whenever necessary. This substitution should not change the program's desired properties.
Let us demonstrate this with a simple example. Assume we have the class Bird, which has two methods: eat() and fly(). Class Eagle has now inherited the Bird class. The Eagle class now owns the eat() and fly() methods. So we can use the object of the Bird class instead of the object of the Eagle class. This will not disrupt the existing functionalities.
Liskov Substitution Principle (LSP) in Java
It sounds simple, and it is. But there's a catch here. This principle applies to classes that exhibit the same behavior. We assume that the Bird and Eagle classes behave similarly. At the same time, the subclass's overridden methods should behave the same way as the superclass's methods. Based on this assumption, we can use the Bird object rather than the Eagle object.

But what happens if the parent and child classes do not behave similarly? What happens if the overridden methods of the subclass do not behave like the superclass's methods. How can we apply the Liskov Substitution Principle in those cases?

Let us consider another example. We will explore this principle with a Java-based Payment Processing System.

Example without LSP

Assume an e-commerce platform has different types of payment methods. Customers can pay with Credit cards, Debit cards, PayPal, or Cash.
The payment classes override the processPayment(...) method. This method will contain logic for processing payments. We may need to consume third-party APIs to settle Credit card or PayPal payments.

But in the case of cash payments, there is no need to process. We can simply accept the cash. As a result, the behavior of precessPayment(...) differs when cash payments are made.
Liskov Substitution Principle (LSP) in Java
The behavior of the processPayment(...) method is not the same in all the subclasses. So this violates the Liskov Substitution Principle.


Example with LSP

To implement the Liskov Substitution Principle, we created two interfaces: IPayment and IOnlinePayment.
IPayment is the interface for all payment types. IOnlinePayment is the interface for all kinds of online payments.

CreditCardPayment and PayPalPayment have implemented the IOnlinePayment interface.
We've overridden the authenticate() and processPayment() methods in the classes.

Here is our class on cash payments. This class only implements the IPayment interface.
It overrides the processPayment() method. Because authentication is not required here, we have not implemented the IOnlinePayment interface.
Liskov Substitution Principle (LSP) in Java
Every class correctly extends Payment's behavior while adhering to expectations.

Happy coding!!! 😊
in

Wednesday, February 19, 2025

Open-Closed Principle (OCP)

java,programming,software development,technology,design principle
The SOLID principles, a set of five design principles that enhance object-oriented programming (OOP). We discussed the Single Responsibility Principle (SRP) in this article.

SOLID Design Principle is made up of five different principles:
  • S: Single Responsibility Principle (SRP)
  • OOpen-Closed Principle (OCP)
  • LLiskov Substitution Principle (LSP)
  • IInterface Segregation Principle (ISP)
  • DDependency Inversion Principle (DIP)
SOLID Design Principles in Java
This blog post reviews the Open-Closed Principle (OCP) and shows how it can be applied using Java examples.

Open-Closed Principle

This is the second principle of the SOLID design principle group. It tells us:
Classes or modules should be open for extension but closed for modification.
It states that we should not modify any existing classes or modules. Rather, we should create a new entity by inheriting the properties of the existing classes and modules. So, this principle tells us to use inheritance and overriding to extend existing behaviors.

Example without OCP

Assume an e-commerce platform has two discount types: flat discount and percentage discount. Here's our discount system. As you can see, the calculateDiscount(...) method is used to compute a discount on the purchase price based on the discount type.
public class DiscountSystem {
    private Double flatDiscountAmount;
    private Double discountPercentage;
    private Double baseDiscountPercentage;

    // CALCULATE DISCOUNT
    public Double calculateDiscount(EDiscountType type, Double amount) {
        Double discountedAmount;

        switch (type.name()) {
            case "FLAT":
                discountedAmount = amount - (amount * baseDiscountPercentage / 100) - flatDiscountAmount;
                break;
            case "PERCENTAGE":
                discountedAmount = amount - (amount * baseDiscountPercentage / 100)
                        - (amount * discountPercentage / 100);
                break;
            default:
                discountedAmount = 0D;
                break;
        }
        return discountedAmount;
    }

    public Double getFlatDiscountAmount() {
        return flatDiscountAmount;
    }

    public DiscountSystem setFlatDiscountAmount(Double flatDiscountAmount) {
        this.flatDiscountAmount = flatDiscountAmount;
        return this;
    }

    public Double getDiscountPercentage() {
        return discountPercentage;
    }

    public DiscountSystem setDiscountPercentage(Double discountPercentage) {
        this.discountPercentage = discountPercentage;
        return this;
    }

    public Double getBaseDiscountPercentage() {
        return baseDiscountPercentage;
    }

    public DiscountSystem setBaseDiscountPercentage(Double baseDiscountPercentage) {
        this.baseDiscountPercentage = baseDiscountPercentage;
        return this;
    }
}

public enum EDiscountType {
    FLAT, PERCENTAGE;
}
Assume the e-commerce platform wants to introduce another discount system. So first, we'll create a new discount type. Then, create a new case block to implement the new discount logic. After a few days, they plan to introduce another new discount system. So, once again, we must take the same steps to implement the new discount logic.

Every time, we modify the existing code to add new business logic. As a result, these frequent changes increase the risk of breaking existing functionality. This is what the Open-Closed Principle (OCP) says to avoid.

Example with OCP

To implement OCP, we must first define an abstract class with an abstract method. Here's our Discount abstract class. This abstract class has its own property and an abstract method called calculateDiscount(...). This abstract method will be overridden in the class that extends the Discount class.
public abstract class Discount {
    private Double baseDiscountPercentage;

    public abstract Double calculateDiscount(Double amount);

    public Double getBaseDiscountPercentage() {
        return baseDiscountPercentage;
    }

    public void setBaseDiscountPercentage(Double discountPercentage) {
        this.baseDiscountPercentage = discountPercentage;
    }
}

The code snippet below is for FlatDiscountService, which extends the Discount class. It also overrides the calculateDiscount(...) method to incorporate its own discount business logic.
public class FlatDiscountService extends Discount {
    private Double flatDiscountAmount;

    public Double getFlatDiscountAmount() {
        return flatDiscountAmount;
    }

    public FlatDiscountService setFlatDiscountAmount(Double discountAmount) {
        this.flatDiscountAmount = discountAmount;
        return this;
    }

    @Override
    public Double calculateDiscount(Double amount) {
        return amount - (amount * super.getBaseDiscountPercentage() / 100) - flatDiscountAmount;
    }
}

We can develop another new discount service. This is PercentageDiscountService that uses the Discount class.
public class PercentageDiscountService extends Discount {
    private Double discountPercentage;

    public Double getDiscountPercentage() {
        return discountPercentage;
    }

    public PercentageDiscountService setDiscountPercentage(Double discountPercentage) {
        this.discountPercentage = discountPercentage;
        return this;
    }

    @Override
    public Double calculateDiscount(Double amount) {
        return amount - (amount * super.getBaseDiscountPercentage() / 100) - (amount * discountPercentage / 100);
    }
}
Open-Closed Principle (OCP)
If the e-commerce platform wants to introduce a new discount system, it simply creates a new class by extending the abstract one. At the same time, our current discount system will remain unchanged.

Here is our test class to test the discount services.
public class OcpGoodExampleMain {
    public static void main(String[] args) {
        Double purchasedAmount = 4710.78;

        FlatDiscountService flatDiscountService = new FlatDiscountService()
                .setFlatDiscountAmount(250.98);
        flatDiscountService.setBaseDiscountPercentage(1D);
        System.out.println("Actual purchased amount: " + purchasedAmount);
        System.out.println("After flat discount: " + flatDiscountService.calculateDiscount(purchasedAmount));

        System.out.println("-------------------------------------");

        purchasedAmount = 7980.98;
        PercentageDiscountService percentageDiscountService = new PercentageDiscountService()
                .setDiscountPercentage(3.6);
        percentageDiscountService.setBaseDiscountPercentage(1.7);
        System.out.println("Actual purchased amount: " + purchasedAmount);
        System.out
                .println("After percentage discount: " + percentageDiscountService.calculateDiscount(purchasedAmount));
    }
}

Using the Open-Closed Principle (COP), new discount types can be added without changing the Discount class. When we add new discount strategies, the core system is left untouched.

The Open-Closed Principle (OCP) can be applied in various real-world software development scenarios. In those cases, we need to add functionality without changing the existing code. Here are some real-world examples.
  1. Payment Systems: Credit card payments are initially accepted by an online store, but PayPal, Apple Pay, and cryptocurrency payments must be added later.
  2. Logging System: An application begins by logging into a text file, but it must eventually support logging into databases, cloud services (such as AWS CloudWatch), or external monitoring tools.
  3. Notification Services: A notification system sends email notifications at first, but it must eventually support SMS, push notifications, and WhatsApp messages.
  4. User Role Management: A web application has an Admin role, but additional roles such as Editor, Viewer, and Moderator with varying permissions must be added.
  5. Machine Learning Model Deployment: A system begins with a simple linear regression model but eventually needs to support deep learning models, decision trees, and ensemble methods.

Happy coding!!! 😊
in

Sunday, February 09, 2025

SOLID Design Principles in Java

java,programming,software development,technology,design principle
Writing clean, maintainable, and scalable code is essential in software development. The SOLID principles, a set of five design principles that enhance object-oriented programming (OOP), are one of the most effective ways to achieve this.

While writing Java code, we should follow the SOLID principle to maintain a better software architecture. This principle reduces code complexity and makes our code more testable.

The five different principles are:
  • S: Single Responsibility Principle (SRP)
  • OOpen-Closed Principle (OCP)
  • LLiskov Substitution Principle (LSP)
  • IInterface Segregation Principle (ISP)
  • DDependency Inversion Principle (DIP)
SOLID Design Principles in Java
This blog post reviews the Single Responsibility Principle (SRP) and shows how it can be applied using Java examples.

Assume a restaurant is run by a single man. He is solely in charge of cooking, selling, shopping, cleaning, and accounting. This allows him to run his restaurant for a limited time. However, over time, he may close his business due to exhaustion and inefficiency.
SOLID Design Principles in Java
So, what will he do to save his business? He ought to recruit help and delegate various tasks to different people.

Single Responsibility Principle (SRP)

This is the first principle of the Solid Design Principles group. It tells us:
There should be no more than one reason for a class to change.
The statement may appear a little complicated. However, this is not the case. A single responsibility indicates that a class should provide or address a specific functionality. A class with multiple responsibilities is subject to several changes.

Consider an image of a neatly organized toolbox. Each tool in this toolbox serves a specific purpose. A wrench is used for tightening bolts, and a hammer for hammering nails. Similarly, each class should have a single, well-defined function in software design.
SOLID Principles In Java

Let us look at a simple Java example:
public class UserService {

	// REGISTER A NEW USER
	public String register(UserDetails registerDetails) {
		System.out.println("User registration is completed successfully.");
		return "SUCCESS";
	}

	// UPDATE USER DETAILS
	public String updateUserDetails(UserDetails details) {
		System.out.println("User details is updated successfully.");
		return "SUCCESS";
	}

	// LOGIN USING USERNAME AND PASSWORD
	public String login(String userName, String password) {
		System.out.println("User credential is verified successfully.");
		return "SUCCESS";
	}

	// SEND OTP TO USER PHONE NUMBER
	public String sendOTP(String phoneNumber) {
		// CONSUME THIRD PARTY API TO SEND OTP VIA SMS
		// ..

		System.out.println("OTP is successfully sent.");
		return "SUCCESS";
	}

	// OTP VALIDATION
	public String validateOTP(String code) {
		// OTP/CODE VALIDATION LOGIC
		// ..

		System.out.println("OTP validation is completed successfully.");
		return "SUCCESS";
	}

	// SSO AUTHENTICATION: TOKEN VALIDATION
	public String validateOAuthToken(String token) {
		// CONSUME THIRD PARTY API TO VALIDATE OAUTH TOKEN
		// ..

		System.out.println("OAurh Token validation is completed successfully.");
		return "SUCCESS";
	}

	// SAVE AUDIT DETAILS - MAINTAIN A LOG FOR THE CHANGES OF USER DETAILS
	public String saveAudit(UserRegisterDetails details) {
		System.out.println("Audit information of user details is saved successfully.");
		return "SUCCESS";
	}
}
We can see that the UserService has methods for:
  • Register a new User
  • Update user details
  • User login using credentials
  • Send OTP to the user's phone number
  • OTP validation
  • OAuth token validation
  • Save audit information - changes in user details

We implemented several functionalities in this class. However, there are many reasons why this class may change. What are some possible reasons for the change in this UserService class?
  • If we add a new property to the UserDetails class, we must modify the register(...) and updateUserDetails(...) methods.
  • To send OTP, we must use third-party SMS API. If the logic for consumption in the SMS API changes, we should modify the sendOTP(...) method.
  • If the logic for OTP validation changes, we must update the validateOTP(..) method.
  • Assume we have implemented Office 365 SSO, which allows users to log in to our application using their Office 365 accounts. We now want users with Google and Github accounts to be able to log in to our application using SSO. So, once again, we must change the validateOAuthToken(...) method.
  • If we want to save audit information other than user details, we must modify the saveAudit(...) method.

So, as you can see, there are several reasons why our class will change. And that is what we should avoid. This is what the Single Responsibility Principle (SRP) says to avoid.

In the software development process, we can not avoid changes in our code. So, whenever there are changes, we should create a separate class or module to handle those responsibilities. This allows us to change our code in an organized manner.

So, when designing a class or module, remember that a class addresses a specific concern. If a change request is received for that class, there can only be one reason for it to change.

To follow the SRP, we divided the responsibilities of the UserService class into multiple classes.

For example, the UserRegistrationService is responsible for registering new user details:
public class UserRegistrationService {

    // REGISTER A NEW USER
    public String register(UserDetails registerDetails) {
        // LOGIC TO SAVE USER DETAILS IN DB
        // ...

        System.out.println("User registration is completed successfully.");
        return "SUCCESS";
    }
}

The UserDetailsUpdateService is responsible for updating existing user details:
public class UserDetailsUpdateService {

    // UPDATE USER DETAILS
    public String updateUserDetails(UserDetails details) {
        // LOGIC TO SAVE/UPDATE USER DETAILS IN DB
        // ...

        System.out.println("User details is updated successfully.");
        return "SUCCESS";
    }
}

In OTPService, we have methods to send and validate OTP.
public class OTPService {

	// SEND OTP TO USER PHONE NUMBER
	public String sendOTP(String phoneNumber) {
		// CONSUME THIRD PARTY API TO SEND OTP VIA SMS
		// ..

		System.out.println("OTP is successfully sent.");
		return "SUCCESS";
	}

	// OTP VALIDATION
	public String validateOTP(String code) {
		// CODE VALIDATION LOGIC
		// ..

		System.out.println("OTP validation is completed successfully.");
		return "SUCCESS";
	}
}

The AuditService class saves audit details.
public class AuditService {

    // SAVE AUDIT DETAILS - MAINTAIN A LOG FOR THE CHANGES IN THE ENTITIES
    public String saveAuditDetails(List<AuditDetails> auditDetails) {
        // LOGIC TO SAVE AUDIT DETAILS IN DB
        // ...

        System.out.println("Audit details saved successfully.");
        return "SUCCESS";
    }
}

Single Responsibility Principle (SRP)
Here we have our main class. The code snippet describes how to use the classes.
public class SingleResponsibilityMain {
    public static void main(String[] args) {

        // REGISTRATION SERVICE
        UserRegistrationService registrationService = new UserRegistrationService();
        registrationService.register(new UserDetails(0L,
                "Silas Ross",
                "silas_ross@gmail.com",
                "9090801010",
                "silas8090ross"));

        // UPDATE SERVICE
        UserDetailsUpdateService updateService = new UserDetailsUpdateService();
        updateService.updateUserDetails(new UserDetails(101L,
                "Silas Ross",
                "silas_6060_ross@gmail.com",
                "9090806060",
                "silas8090ross"));

        // LOG/AUDIT SERVICE
        AuditService auditService = new AuditService();
        auditService.saveAuditDetails(List.of(new AuditDetails(LocalDateTime.now(),
                "email",
                "silas_ross@gmail.com",
                "silas_6060_ross@gmail.com",
                "user",
                109L),
                new AuditDetails(LocalDateTime.now(),
                        "phone",
                        "9090801010",
                        "9090806060",
                        "user",
                        109L)));

        // OTP SERVICE
        OTPService otpService = new OTPService();
        otpService.sendOTP("9000000010");
        otpService.validateOTP("983417");
    }
}
Here you can see that we tested each class separately.

Using the Single Responsibility Principle, we divided a class's responsibilities or functionalities. This makes our code more modular, flexible, and maintainable. We can also test each class individually. Each class now has only one reason to change, as per the Single Responsibility Principle.

Happy coding!!! 😊
in

Monday, December 30, 2024

RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange

java,rabbitmq,message broker,programming,software development,technology,software engineering
In this article, we will learn about the messaging patterns of message brokers. RabbitMQ supports a wide range of messaging patterns one of which is the Publisher-Subscriber (Pub-Sub) pattern. In the previous article, we learned what a message broker is.

Pub/Sub Pattern

Assume I've published an article on Medium. One Medium user liked my article and gave me a clap👏. Now Medium would do the following: increase the clap count for that article by one, notify me via email that one user had clapped my article, and pay me 10 cents(!).
RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange
So, based on a single event, different services are triggered. In the same way, in the Pub-Sub pattern, a producer sends a message to an exchange, which then sends the same message to multiple consumers. The reason for doing so is that different services in the microservice architecture may be interested in processing the same message. Similar to the preceding example, the various services process the clapping information differently.
RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange
Consider another example. When a user creates a new account in an application, the new user information is stored in one microservice. This data may be of interest to the logging or auditing microservice. And the promotional service would use these user details to send promotional emails.

Exchange is extremely important in the Pub-Sub pattern. In the previous article, the producer is connected to a specific queue via the default exchange. However, in the Pub-Sub pattern, the producer can only send messages to the exchange. The exchange must know what to do with the message that arrives. RabbitMQ provides various exchange types to help you decide what to do with the message. The available exchange types are listed below.
  • Direct
  • Topic
  • Header
  • Fanout
The Fanout exchange publishes the same message to multiple queues. We will use a Fanout exchange to implement the Pub-Sub pattern.

Random Queue Allocation

In our previous article, we described how the producer communicates with the consumer via a specified queueThis means whenever the producer wants to exchange messages it must use that particular queue to send messages to consumers.

However, in this pattern, we are only interested in current messages, not old ones. To accomplish this, we could create a queue with a random name or allow the channel to select a random queue name. The queue is automatically deleted once the consumer disconnects from the exchange.
RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange

Implementation using Java

Shared Component Module

The Shared Component Module contains the MessageBody class. It consists of two records: UserDetails and ArticleDetails.
package com.raven.components.model;

public record UserDetails(
        String name,
        String email
) {
}

package com.raven.components.model;

public record ArticleDetails(
        String length,
        String publicationDate,
        Boolean isClapped,
        String clappedBy,
        Integer readTime,
        String timeUnit
) {
}

package com.raven.components.model;

public class MessageBody {
    private UserDetails userDetails;
    private ArticleDetails articleDetails;

    public UserDetails getUserDetails() {
        return userDetails;
    }

    public MessageBody setUserDetails(UserDetails userDetails) {
        this.userDetails = userDetails;
        return this;
    }

    public ArticleDetails getArticleDetails() {
        return articleDetails;
    }

    public MessageBody setArticleDetails(ArticleDetails articleDetails) {
        this.articleDetails = articleDetails;
        return this;
    }
}
We will use MessageBody to transport data between producers and consumers.

This module also includes the UtilityService class, which contains a variety of conversion methods.
package com.raven.components.utility;

public class UtilityService {

    public static byte[] convertObjectToByte() {
        var messageBody = getMessage();
        var mapper = new ObjectMapper();
        byte[] _byte = new byte[0];

        try {
            String message = mapper.writeValueAsString(messageBody);
            _byte = message.getBytes(StandardCharsets.UTF_8);
        } catch (JsonProcessingException e) {
            System.out.println("Error in processing the JSON : " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }

        return _byte;
    }

    public static byte[] convertObjectToByte(MessageBody messageBody) {
        var mapper = new ObjectMapper();
        byte[] _byte = new byte[0];

        try {
            String message = mapper.writeValueAsString(messageBody);
            _byte = message.getBytes(StandardCharsets.UTF_8);
        } catch (JsonProcessingException e) {
            System.out.println("Error in processing the JSON : " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }

        return _byte;
    }

    public static String convertObjectToString() {
        var messageBody = getMessage();
        var mapper = new ObjectMapper();
        String message = "";

        try {
            message = mapper.writeValueAsString(messageBody);
        } catch (JsonProcessingException e) {
            System.out.println("Error in processing the JSON : " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }

        return message;
    }

    public static String convertObjectToString(MessageBody messageBody) {
        var mapper = new ObjectMapper();
        String message = "";

        try {
            message = mapper.writeValueAsString(messageBody);
        } catch (JsonProcessingException e) {
            System.out.println("Error in processing the JSON : " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }

        return message;
    }

    public static MessageBody convertStringToObject(String message) {
        var mapper = new ObjectMapper();
        MessageBody messageBody = new MessageBody();

        try {
            messageBody = mapper.readValue(message, new TypeReference<MessageBody>() {
            });
        } catch (JsonProcessingException e) {
            System.out.println("Error in processing the JSON : " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }

        return messageBody;
    }

    public static MessageBody convertStringToObject(byte[] bytes) {
        var mapper = new ObjectMapper();
        MessageBody messageBody = new MessageBody();

        try {
            String message = new String(bytes, StandardCharsets.UTF_8);
            messageBody = mapper.readValue(message, new TypeReference<MessageBody>() {
            });
        } catch (JsonProcessingException e) {
            System.out.println("Error in processing the JSON : " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }

        return messageBody;
    }

    private static MessageBody getMessage() {
        return new MessageBody()
                .setUserDetails(new UserDetails(
                        "Paula Small",
                        "paula.small@bilearner.com")
                )
                .setArticleDetails(new ArticleDetails(
                        "1289 words",
                        "03/12/2024",
                        true,
                        "john.dow@yahoomail.com",
                        80,
                        "second")
                );
    }
}
We will use these conversion methods to send and receive messages between producers and consumers.

Producer

To implement the Pub-Sub pattern, we defined an exchange of type FANOUT. We are publishing the MessageBody object to the exchange. As you can see, we are not attaching a queue to the channel. The channel is directly connected to the exchange.
package com.raven.producer;

public class MyProducer {
    private static final String HOST = "localhost";
    private static final String EXCHANGE = "pub_sub";

    public static void main(String[] args) {
        // CREATE A CONNECTION FACTORY
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(HOST);

        // CREATE A CONNECTION FROM FACTORY
        try (Connection connection = connectionFactory.newConnection()) {
            // GET CHANNEL FROM CONNECTION
            Channel channel = connection.createChannel();

            // DECLARE A EXCHANGE
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.FANOUT);

            // MESSAGE DETAILS
            var message = UtilityService.convertObjectToString(
                    new MessageBody()
                            .setUserDetails(new UserDetails(
                                    "Paula Small",
                                    "paula.small@bilearner.com")
                            )
                            .setArticleDetails(new ArticleDetails(
                                    "1289 words",
                                    "03/12/2024",
                                    true,
                                    "john.dow@yahoomail.com",
                                    80,
                                    "second")
                            )
            );

            // PUBLISH A MESSAGE TO CHANNEL
            channel.basicPublish(EXCHANGE, "", null, message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" Message sent : '" + message + "'");
            System.out.println();

        } catch (Exception e) {
            System.out.println("Error in sending message: " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }
    }
}

Consumer #1

Here is our first consumer: Article service. Here, we are processing the article's clap information. The queueDeclare() method is used to declare an auto-delete, non-durable queue. It returns the queue name once the queue has been successfully created. The queue is then bound to the exchange with the queueBind() method.
package com.raven.article_cosumer;

public class ArticleConsumer {
    private static final String HOST = "localhost";
    private static final String EXCHANGE = "pub_sub";

    public static void main(String[] args) {
        // CREATE A CONNECTION FACTORY
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(HOST);

        try {
            // CREATE A CONNECTION
            Connection connection = connectionFactory.newConnection();

            // CREATE A CHANNEL FROM CONNECTION
            Channel channel = connection.createChannel();

            // DECLARE A EXCHANGE
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.FANOUT);

            // DECLARE A QUEUE IN THE CHANNEL AND GET ITS NAME
            String queueName = channel.queueDeclare().getQueue();
            System.out.println("Consumer: Article Service: Queue name : " + queueName);

            // BIND THE QUEUE WITH THE EXCHANGE
            channel.queueBind(queueName, EXCHANGE, "");

            System.out.println("Waiting for message...");

            DeliverCallback deliverCallback = (consumeMsg, delivery) -> {
                String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println("Consumer: Article Service :: Message received : '" + message + "'");

                System.out.println();
                var messageBody = UtilityService.convertStringToObject(message);

                ArticleDetails articleDetails = messageBody.getArticleDetails();
                System.out.println("-> Article details: " + articleDetails);

                if (articleDetails.isClapped()) {
                    System.out.println("-> Article is clapped by " + articleDetails.clappedBy());
                    System.out.println("-> And article has 67 claps now!");
                }
            };

            // GET MESSAGE FROM EXCHANGE
            channel.basicConsume(queueName, true, deliverCallback, consumeMsg -> {
            });
        } catch (Exception e) {
            System.out.println("Article Consumer: Error in consuming message: " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }
    }
}

Consumer #2

Payment service is our second consumer. This consumer handles the payment information. Here, we create the queue at runtime and associate it with the exchange.
package com.raven.payment_cosumer;

public class PaymentConsumer {
    private static final String HOST = "localhost";
    private static final String EXCHANGE = "pub_sub";

    public static void main(String[] args) {
        // CREATE A CONNECTION FACTORY
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(HOST);

        try {
            // CREATE A CONNECTION
            Connection connection = connectionFactory.newConnection();

            // CREATE A CHANNEL FROM CONNECTION
            Channel channel = connection.createChannel();

            // DECLARE A EXCHANGE
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.FANOUT);

            // DECLARE A QUEUE IN THE CHANNEL AND GET ITS NAME
            String queueName = channel.queueDeclare().getQueue();
            System.out.println("Consumer: Payment Service: Queue name : " + queueName);

            // BIND THE QUEUE WITH THE EXCHANGE
            channel.queueBind(queueName, EXCHANGE, "");

            System.out.println("Waiting for message...");

            DeliverCallback deliverCallback = (consumeMsg, delivery) -> {
                String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println("Consumer: Article Service :: Message received : '" + message + "'");

                System.out.println();
                var messageBody = UtilityService.convertStringToObject(message);

                ArticleDetails articleDetails = messageBody.getArticleDetails();
                System.out.println("-> Article details: " + articleDetails);

                if (articleDetails.isClapped() && articleDetails.readTime() > 50) {
                    System.out.println("-> Article is read time is " + articleDetails.readTime() + articleDetails.timeUnit());
                    System.out.println("-> You have received 30 cents.");
                    System.out.println("-> Your total earning is $10.45.");
                }
            };

            // GET MESSAGE FROM EXCHANGE
            channel.basicConsume(queueName, true, deliverCallback, consumeMsg -> {
            });
        } catch (Exception e) {
            System.out.println("Payment Consumer: Error in consuming message: " + e.getMessage() + ", " + Arrays.toString(e.getStackTrace()));
        }
    }
}

Testing

We need to install RabbitMQ on our local machine to communicate with the Java application. You can find an installation guide here. So, we'll start our two consumer applications one at a time, then the producer program.

The producer sends the message.
RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange

The article service consumer receives and processes the message.
RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange

The payment service consumer also receives and processes the message.
RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange

The RabbitMQ management console is accessible via http://localhost:15672/#/. To log in to the console, use guest as the username and password. Click the 'Exchanges' tab to view the exchanges that are linked to the message broker. Find our exchange, 'pub_sub', and click on it for more information.
RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange

To view the queues, select the 'Queues and Streams' tab.
RabbitMQ - Pub-Sub Pattern: Implementation Of Fanout Exchange

Source code: PubSubDemo.
Happy coding!!! 😊
in

Saturday, November 23, 2024

RabbitMQ: Message Broker - A Brief Introduction

We are all familiar with Hogwarts' magical postal network, the Owl Post Office. This magical delivery system relies on owls to deliver packages and letters. They can even pick up an item from any address and give it to another using their magical tracking abilities. Similarly, we can compare RabbitMQ, a message broker to the Owl Post Office. It allows applications to interact with one another and exchange messages.

But why should we use message broker?

An e-commerce application ships thousands of items per day and sends email notifications for each one. This is a synchronous operation. This means that the item's ship status is saved in the database, and an email is sent. Now, on bad days, the email server goes down or crashes due to overload. So none of us receive the email notification.
RabbitMQ -  A Brief Introduction

Assume we add a service layer between the shipment service and e-mail service. So, shipping service sends the shipment notification to the service layer, which will route the message to the email service. If the email service goes down, the service layer will store the messages. When the email service goes live, the service layer will push those messages to the e-mail service.
RabbitMQ -  A Brief Introduction
And this service layer is nothing but a message broker.

RabbitMQ is a distributed message and stream broker. A message broker is software that sits among applications, allowing them to exchange messages. RabbitMQ is useful for decoupling services, remote procedure calls (RPC), streaming services, and IoT.

RabitMQ is most commonly used in microservice-based architectures. It operates asynchronously. This means they do not follow a simple request-response pattern, and we must wait for replies. RabbitMQ, like the postal service, sends messages from producer to consumer.
RabbitMQ -  A Brief Introduction

Producer

A Producer is an action or event that generates messages. Credit card transactions, a drop or rise in stock price, or an order dispatch are all examples of Producers.

Consumer

On the other hand, Consumers are the entities that listen to messages. Humans are the perfect example of a consumer. We consume everything, from news to alcohol. Jokes aside! Because a message broker might be connected to multiple producers and consumers, communication between them is asynchronous.

Exchange

But, wait a second! How does the message broker get a message from the producer to the consumer? The answer is Exchange. It functions as RabbitMQ's brain. Exchange helps message broker to route messages from producer to consumer.
RabbitMQ -  A Brief Introduction

Queue & Binding

A message broker may have multiple exchanges. Exchanges always receive messages from producers. Consumers are not connected directly to exchanges. Queues connect exchanges and consumers. Binding connects queues to exchanges. You can think of queues like our letterbox. Exchange pushes messages into queues, from which interested consumers can consume them.
RabbitMQ -  A Brief Introduction
An exchange can be bound to multiple queues, and a queue can be linked to numerous exchanges. The consumer might also listen to messages from multiple queues.

Connection & Channel

RabbitMQ is designed to implement the AMQP (Advance Message Queuing Protocol). AMQP is an open messaging protocol that defines the rules for message exchange, queueing, and routing in a messaging system.

To communicate with RabbitMQ, a client or application must first establish a connection. A client can be either a producer or a consumer. The connection is established using either TCP or TLS. The main purpose of a connection is to establish a secure path between the client and RabbitMQ.

A connection can have several channels. But, why do we need channels? We can set up multiple connections between the client and the broker to exchange messages. Keeping multiple TCP connections open at the same time is undesirable because it consumes system resources and makes firewall configuration more difficult. So, according to the AMQP protocol, channels are "lightweight connections that share a single TCP connection".
RabbitMQ -  A Brief Introduction
We can use the amqp-client Java library to communicate with RabbitMQ in a Java application. This library is available through the Maven repository. We need to install RabbitMQ on our local machine to communicate with the Java application. You can find an installation guide here.

Producer & Consumer using Java

Now, let's look at how to create a message and how to consume it using RabbitMQ and Java. First, we write a program that will connect and publish a message to RabbitMQ.
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.nio.charset.StandardCharsets;

public class Publisher {
    private static final String HOST = "localhost";
    private static final String QUEUE = "OWL-POST";
    private static final String MESSAGE = "Happy birthday Hermione!";

    public static void main(String[] args) {
        // CREATE A CONNECTION FACTORY
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(HOST);

        // CREATE A CONNECTION FROM FACTORY
        try (Connection connection = connectionFactory.newConnection()) {
            // GET CHANNEL FROM CONNECTION
            Channel channel = connection.createChannel();

            // ASSIGN A QUEUE TO CHANNEL
            channel.queueDeclare(QUEUE, false, false, false, null);

            // PUBLISH A MESSAGE TO CHANNEL
            channel.basicPublish("", QUEUE, null, MESSAGE.getBytes(StandardCharsets.UTF_8));
            System.out.println(" Message sent : '" + MESSAGE + "'");
        } catch (Exception e) {
            System.out.println(e.getMessage() + ", " + e.getStackTrace());
        }
    }
}
We have used amqp-client Java client library to communicate with RabbitMQ.

RabbitMQ is running on our local machine, so the host is localhost. The queue name is 'OWL-POST'. Consumer should connect to this queue to consume message from producer.

ConnectionFactory is a factory class that allows you to open a connection to RabbitMQ.

First, we get a connection from ConnectionFactory, and then we create a channel. We create a queue using queueDeclare() method by passing the queue name. This queue is idempotent, which means that if it already exists under this name, it will not be created again.

Next, we use the basicPublish() method to send our encoded message to the queue. The first argument of the basicPublish() method is the exchange name. Because we passed a blank string as the first argument, we are connecting to RabbitMQ's default exchange.

Here is our consumer.
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;

public class Consumer {
    private static final String HOST = "localhost";
    private static final String QUEUE = "OWL-POST";

    public static void main(String[] args) {
        // CREATE A CONNECTION FACTORY
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(HOST);

        try {
            // CREATE A CONNECTION
            Connection connection = connectionFactory.newConnection();

            // CREATE A CHANNEL FROM CONNECTION
            Channel channel = connection.createChannel();

            // ASSIGN A QUEUE TO CHANNEL
            channel.queueDeclare(QUEUE, false, false, false, null);
            System.out.println(" Waiting for message...");

            DeliverCallback deliverCallback = (consumeMsg, delivery) -> {
                String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println(" Message received : '" + message + "'");
            };

            channel.basicConsume(QUEUE, true, deliverCallback, consumeMsg -> {});
        } catch (Exception e) {
            System.out.println(e.getMessage() + ", " + e.getStackTrace());
        }
    }
}
So the consumer is connected to the 'OWL-POST' queue. And it receives the message via the basicConsume() method.

DeliverCallback is a callback interface. It is notified when the consumer receives the message and is passed as an argument to the basicConsume() method.

Here is the message from Harry -'Happy birthday Hermione!'.

Source code: Publisher and Consumer.
Happy coding!!! 😊

Popular posts