DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Jakarta NoSQL 1.0: A Way To Bring Java and NoSQL Together
  • Finally, an ORM That Matches Modern Architectural Patterns!
  • High-Performance Reactive REST API and Reactive DB Connection Using Java Spring Boot WebFlux R2DBC Example
  • Contexts in Go: A Comprehensive Guide

Trending

  • Scaling Mobile App Performance: How We Cut Screen Load Time From 8s to 2s
  • Analyzing “java.lang.OutOfMemoryError: Failed to create a thread” Error
  • Enhancing Security With ZTNA in Hybrid and Multi-Cloud Deployments
  • Understanding and Mitigating IP Spoofing Attacks
  1. DZone
  2. Data Engineering
  3. Databases
  4. Fluent-API: Creating Easier, More Intuitive Code With a Fluent API

Fluent-API: Creating Easier, More Intuitive Code With a Fluent API

This article will cover the fluent API, what it is, and how you can take advantage of this design to make the code cleaner and more valuable.

By 
Otavio Santana user avatar
Otavio Santana
DZone Core CORE ·
Updated Jun. 22, 23 · Tutorial
Likes (7)
Comment
Save
Tweet
Share
24.3K Views

Join the DZone community and get the full member experience.

Join For Free

In software development, writing clean and readable code is a top priority for developers. Not only does it make maintenance and collaboration more manageable, but it also enhances the overall value of the codebase. Using a Fluent API is a powerful design approach that promotes clean and expressive code.

In this article, we will delve into the concept of a Fluent API and explore how it can transform your code into something that is easier to read and more intuitive to use. A Fluent API allows developers to build code that resembles natural language, making it highly readable and self-explanatory.

But what exactly is a Fluent API? It is an interface or set of methods carefully designed to provide a smooth and fluid coding experience. By leveraging method chaining and an intuitive naming convention, a Fluent API allows you to write code that reads like a sentence, with each method call seamlessly flowing into the next.

The benefits of a Fluent API are twofold. First, it improves the readability of your code by eliminating unnecessary syntax noise and making the intent of the code crystal clear. Second, it enhances the developer experience by providing a more intuitive and discoverable API surface. It reduces the learning curve for new team members and makes the codebase more maintainable in the long run.

This article will explore various techniques and best practices for designing and implementing a Fluent API. We will discuss leveraging method chaining, appropriate naming conventions, and meaningful return types to create a fluent and expressive API that adds value to your codebase.

Whether you are developing a library, a framework, or an application, adopting a Fluent API design approach can significantly improve the quality and maintainability of your code. By the end of this article, you will have a solid understanding of what a Fluent API is, why it is valuable, and how you can start implementing it in your projects.

What Is a Fluent-API?

When we speak in the context of software engineering, a fluent API is an object-oriented API whose design is largely based on method chaining.

This concept, created in 2005 by Eric Evans and Martin Fowler, aims to increase code readability by creating a domain-specific language (DSL).

In practice, creating a fluent API means developing an API in which it is unnecessary to memorize the next steps or methods, allowing for a natural and continuous sequence as if it were a menu of options.

This natural cadence works similarly to a restaurant or even a fast-food chain in that as you are putting together a dish, the options vary according to your choices. If, for example, you choose a chicken sandwich, the sides will be suggested considering the chosen dish and so on.

Fluent API in Java Context

In the Java world, we can think of two famous examples of this type of implementation.

The first is the JOOQ framework, a project led by Lukas Eder, which facilitates communication between Java and relational databases. JOOQ's most significant difference is that it is data-oriented, which helps avoid and/or reduce the impedance problem, or loss, associated with relational and object-oriented.

Java
 
Query query = create.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
                    .from(BOOK)
                    .join(AUTHOR)
                    .on(BOOK.AUTHOR_ID.eq(AUTHOR.ID))
                    .where(BOOK.PUBLISHED_IN.eq(1948));

String sql = query.getSQL();
List<Object> bindValues = query.getBindValues();


Another example is in non-relational databases, i.e., NoSQL, within the specifications of the enterprise Java world. Among them is Jakarta EE, which is the first specification of its kind and which became Jakarta NoSQL under the umbrella of the Eclipse Foundation.

The purpose of this specification is to ensure smooth communication between Java and NoSQL databases.

Java
 
DocumentQuery query = select().from("Person").where(eq(Document.of("_id", id))).build();
Optional<Person> person = documentTemplate.singleResult(query);
System.out.println("Entity found: " + person);


Generally speaking, a fluent API is divided into three parts:

1. The final object or result: The fluent API resembles the builder pattern, but the most powerful dynamic is coupled with DSL. In both, the result tends to be an instance representing the result of a process or a new entity.

2. The options: In this case, these are the collections of interfaces or classes that will serve as "our interactive menu." From an action, the idea is that show only the options available for the next step, following an intuitive sequence.

3. The result: After all this process, the answer may or may not result in an instance either for an entity, strategy, etc. The critical point is that the result must be valid.

Fluid API in Practice

To demonstrate some of this concept, we'll create a sandwich order with the expected result of an order with the corresponding purchase price. The flow will be as shown below.

Fluid API Flow Example

Of course, there are several ways to implement this fluent API functionality, but we opted for a short version.

As we already mentioned, the three parts of an API — object, options, and result — we will start with the order that the "Order" interface will represent. A highlight is that this interface has some interfaces which will be responsible for demonstrating our options.

Java
 
public interface Order {


    interface SizeOrder {
        StyleOrder size(Size size);
    }

    interface StyleOrder {

        StyleQuantityOrder vegan();

        StyleQuantityOrder meat();
    }

    interface StyleQuantityOrder extends DrinksOrder {
        DrinksOrder quantity(int quantity);
    }


    interface DrinksOrder {
        Checkout softDrink(int quantity);

        Checkout cocktail(int quantity);

        Checkout softDrink();

        Checkout cocktail();

        Checkout noBeveragesThanks();
    }

    static SizeOrder bread(Bread bread) {
        Objects.requireNonNull(bread, "Bread is required o the order");
        return new OrderFluent(bread);
    }


The result of this API will be our order class. It will contain the sandwich, the drink, and their respective quantities.

A Quick Add-on Before We Go Back to the Tutorial

One point that we won't focus on in this article, but that is worth mentioning, is related to the representation of money.

When it comes to numerical operations, it is ideal to use BigDecimal. That's because, following references like the Java Effective book and the blog When Make a Type, we understand that complex types need a unique type. This reasoning, coupled with the pragmatism of "don't repeat yourself" (DRY, or don't repeat yourself), the result is the use of the Java specification for money: The Money API.

Java
 
import javax.money.MonetaryAmount;
import java.util.Optional;

public class Checkout {

    private final Sandwich sandwich;

    private final int quantity;

    private final Drink drink;

    private final int drinkQuantity;

    private final MonetaryAmount total;

  //...
}


The last step in the journey is API implementation. It will be responsible for the "ugly" part of the code, making the API look beautiful.

The price list will be placed directly in the code since we don't use a database or another data reference, and we intend to make the example as simple as possible. But it is worth stressing that, in a natural environment, this information would be in a database or a service.

Java
 
import javax.money.MonetaryAmount;
import java.util.Objects;

class OrderFluent implements Order.SizeOrder, Order.StyleOrder, Order.StyleQuantityOrder, Order.DrinksOrder {

    private final PricingTables pricingTables = PricingTables.INSTANCE;

    private final Bread bread;

    private Size size;

    private Sandwich sandwich;

    private int quantity;

    private Drink drink;

    private int drinkQuantity;

    OrderFluent(Bread bread) {
        this.bread = bread;
    }

    @Override
    public Order.StyleOrder size(Size size) {
        Objects.requireNonNull(size, "Size is required");
        this.size = size;
        return this;
    }

    @Override
    public Order.StyleQuantityOrder vegan() {
        createSandwich(SandwichStyle.VEGAN);
        return this;
    }

    @Override
    public Order.StyleQuantityOrder meat() {
        createSandwich(SandwichStyle.MEAT);
        return this;
    }

    @Override
    public Order.DrinksOrder quantity(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("You must request at least one sandwich");
        }
        this.quantity = quantity;
        return this;
    }

    @Override
    public Checkout softDrink(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("You must request at least one sandwich");
        }
        this.drinkQuantity = quantity;
        this.drink = new Drink(DrinkType.SOFT_DRINK, pricingTables.getPrice(DrinkType.SOFT_DRINK));
        return checkout();
    }

    @Override
    public Checkout cocktail(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("You must request at least one sandwich");
        }
        this.drinkQuantity = quantity;
        this.drink = new Drink(DrinkType.COCKTAIL, pricingTables.getPrice(DrinkType.COCKTAIL));
        return checkout();
    }

    @Override
    public Checkout softDrink() {
        return softDrink(1);
    }

    @Override
    public Checkout cocktail() {
        return cocktail(1);
    }

    @Override
    public Checkout noBeveragesThanks() {
        return checkout();
    }

    private Checkout checkout() {
        MonetaryAmount total = sandwich.getPrice().multiply(quantity);
        if (drink != null) {
            MonetaryAmount drinkTotal = drink.getPrice().multiply(drinkQuantity);
            total = total.add(drinkTotal);
        }
        return new Checkout(sandwich, quantity, drink, drinkQuantity, total);
    }

    private void createSandwich(SandwichStyle style) {
        MonetaryAmount breadPrice = pricingTables.getPrice(this.bread);
        MonetaryAmount sizePrice = pricingTables.getPrice(this.size);
        MonetaryAmount stylePrice = pricingTables.getPrice(SandwichStyle.VEGAN);
        MonetaryAmount total = breadPrice.add(sizePrice).add(stylePrice);
        this.sandwich = new Sandwich(style, this.bread, this.size, total);
    }
}


The result is an API that will return the request to us straightforwardly and intuitively.

Java
 
Checkout checkout = Order.bread(Bread.PLAIN)
           .size(Size.SMALL)
           .meat()
           .quantity(2)
           .softDrink(2);


How Is Fluent-API Different From Other Patterns?

It is widespread to have a comparison between two API standards, which are Builder and Fluent-API. The reason is that they both use methods in sequence for the process of creating an instance.

However, Fluent-API is "tied with a DSL," and it forces an easy path to do so. But to make these differences even more evident, we've separated highlights for each of these patterns:

1. The Builder Pattern

  • It tends to be much easier to implement;
  • It is not clear which construction methods are needed;
  • The vast majority of problems will happen at runtime;
  • Some tools and frameworks create it automatically;
  • It needs more robust validation in the build method to check which mandatory methods were not invoked.

2. The Fluent-API

  • It is important that, for each method, there is validation and throws the error if the parameter is invalid, remembers the fast fail premise;
  • It must return a valid object at the end of the process.

And now, is it easier to understand the similarities and differences between patterns?

That is our introduction to the concept of fluent API. As with all solutions, there is no "silver bullet," as great complexity is often not justified for the entire process.

Our colleague Rafael Ponte often says, "The easier it is to use the API for the client, the more difficult it will be to design and implement it for the developer."

It is an excellent tool that helps in creating a failsafe for you and other users. You can see more about it on Sou Java's Github.

API Java (programming language) Relational database Database Domain-Specific Language Interface (computing)

Opinions expressed by DZone contributors are their own.

Related

  • Jakarta NoSQL 1.0: A Way To Bring Java and NoSQL Together
  • Finally, an ORM That Matches Modern Architectural Patterns!
  • High-Performance Reactive REST API and Reactive DB Connection Using Java Spring Boot WebFlux R2DBC Example
  • Contexts in Go: A Comprehensive Guide

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • [email protected]

Let's be friends: