Index

Building Domain-Specific Languages (DSLs)

Java Functional Programming

15.1 What is a DSL?

A Domain-Specific Language (DSL) is a specialized programming language tailored to express solutions and logic within a particular problem domain. Unlike general-purpose programming languages (e.g., Java, Python), DSLs focus on providing concise, readable, and expressive syntax that matches the terminology and concepts familiar to domain experts.

Why Use DSLs?

DSLs improve productivity and maintainability by:

For example, a SQL query language is a classic DSL for managing databases, focusing specifically on querying data without worrying about implementation details.

Internal vs External DSLs

DSLs come in two main flavors:

Characteristics of Good DSLs

A well-designed DSL should have:

Simple Illustrative Example

Consider a DSL for filtering orders by status and amount in an e-commerce system.

Traditional Java code might look like this:

List<Order> filtered = orders.stream()
    .filter(o -> o.getStatus().equals("SHIPPED"))
    .filter(o -> o.getAmount() > 100)
    .collect(Collectors.toList());

An internal DSL designed with fluent methods could look like:

List<Order> filtered = OrderQuery.from(orders)
    .statusIs("SHIPPED")
    .amountGreaterThan(100)
    .execute();

This reads more clearly, directly expressing the domain intent without exposing stream details.

Summary

DSLs empower developers to write domain logic that is easier to read, write, and maintain. Internal DSLs built with Java’s functional features strike a great balance by embedding expressive, fluent APIs directly into the language, making complex domains approachable and code more expressive.

Index

15.2 Using Lambdas to Create Fluent APIs

Java’s introduction of lambdas and functional interfaces has transformed how we build expressive, fluent APIs that resemble internal domain-specific languages (DSLs). These APIs allow developers to write code that is readable, flexible, and concise—capturing domain intent while hiding boilerplate.

Fluent APIs and DSLs

A fluent API lets you chain method calls naturally, often resembling a sentence or domain-specific instruction. Combined with lambdas, fluent APIs can embed behavior directly, enabling custom logic passed inline without needing verbose anonymous classes.

How Lambdas Help

Lambdas reduce verbosity by replacing boilerplate code with concise functions. Functional interfaces such as Predicate<T>, Function<T,R>, and custom single-method interfaces enable passing behavior as parameters, configuring how a fluent API operates at runtime.

For example, consider a filtering API:

public interface Filter<T> {
    boolean test(T t);
}

public class FilterBuilder<T> {
    private Predicate<T> predicate = t -> true;

    public FilterBuilder<T> where(Predicate<T> condition) {
        predicate = predicate.and(condition);
        return this;
    }

    public List<T> apply(List<T> items) {
        return items.stream()
                    .filter(predicate)
                    .collect(Collectors.toList());
    }
}

Usage with lambdas:

List<String> data = List.of("apple", "banana", "avocado", "blueberry");

List<String> result = new FilterBuilder<String>()
    .where(s -> s.startsWith("a"))
    .where(s -> s.length() > 5)
    .apply(data);

System.out.println(result); // [avocado]

Here, lambdas allow callers to flexibly define conditions inline, while method chaining creates a fluent, readable API.

Click to view full runnable Code

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

// Functional interface (optional since Predicate<T> is already functional)
interface Filter<T> {
    boolean test(T t);
}

// Builder class using Predicate<T> chaining
class FilterBuilder<T> {
    private Predicate<T> predicate = t -> true;

    public FilterBuilder<T> where(Predicate<T> condition) {
        predicate = predicate.and(condition);
        return this;
    }

    public List<T> apply(List<T> items) {
        return items.stream()
                    .filter(predicate)
                    .collect(Collectors.toList());
    }
}

public class FilterBuilderDemo {
    public static void main(String[] args) {
        List<String> data = List.of("apple", "banana", "avocado", "blueberry");

        List<String> result = new FilterBuilder<String>()
            .where(s -> s.startsWith("a"))
            .where(s -> s.length() > 5)
            .apply(data);

        System.out.println(result); // Output: [avocado]
    }
}

Builder Pattern and Method Chaining

The builder pattern pairs naturally with lambdas to create complex configurations step-by-step. Each method returns this or another builder, enabling chainable calls.

Example: configuring a report generator:

public class ReportBuilder {
    private String title;
    private Consumer<String> formatter;

    public ReportBuilder title(String title) {
        this.title = title;
        return this;
    }

    public ReportBuilder format(Consumer<String> formatter) {
        this.formatter = formatter;
        return this;
    }

    public void generate() {
        String report = "Report: " + title;
        if (formatter != null) {
            formatter.accept(report);
        } else {
            System.out.println(report);
        }
    }
}

Usage:

new ReportBuilder()
    .title("Sales Q2")
    .format(r -> System.out.println("Formatted: " + r.toUpperCase()))
    .generate();

Function Composition

Functional interfaces support composition (andThen, compose) enabling DSLs that build pipelines of behavior dynamically. For instance:

Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> pipeline = trim.andThen(upper);

System.out.println(pipeline.apply("  hello world  ")); // "HELLO WORLD"

This composition style allows creating flexible, reusable DSL components.

Summary

By leveraging lambdas, method chaining, builders, and function composition, Java developers can craft fluent APIs that feel like internal DSLs. These APIs are more concise, flexible, and expressive than traditional patterns, making complex configuration or querying tasks more intuitive and maintainable.

Index

15.3 Example: Building a Simple Query DSL

Creating a simple query DSL (Domain-Specific Language) using lambdas and fluent method chaining demonstrates how Java can embed expressive, readable domain logic directly in code. This example models a query over a collection of Person objects with customizable filters and transformations.

Defining the DSL

We start by defining a Query<T> interface representing a composable query. We use functional interfaces to capture predicates (filters) and mappers (transformations):

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class QueryDSL {

    // Core Query interface with fluent methods
    public static class Query<T> {
        private Stream<T> stream;

        private Query(Collection<T> source) {
            this.stream = source.stream();
        }

        // Static factory method to start query from a collection
        public static <T> Query<T> from(Collection<T> source) {
            return new Query<>(source);
        }

        // Filter with a predicate lambda
        public Query<T> where(Predicate<T> predicate) {
            stream = stream.filter(predicate);
            return this;
        }

        // Map to a different type with a mapper function
        public <R> Query<R> select(Function<T, R> mapper) {
            Stream<R> mappedStream = stream.map(mapper);
            Query<R> newQuery = new Query<>(List.of()); // dummy source for constructor
            newQuery.stream = mappedStream;
            return newQuery;
        }

        // Collect results into a list
        public List<T> execute() {
            return stream.collect(Collectors.toList());
        }
    }

    // Sample domain class
    static class Person {
        String name;
        int age;

        Person(String name, int age) { this.name = name; this.age = age; }

        @Override
        public String toString() {
            return name + " (" + age + ")";
        }
    }

    // Sample usage
    public static void main(String[] args) {
        List<Person> people = List.of(
            new Person("Alice", 30),
            new Person("Bob", 20),
            new Person("Charlie", 25)
        );

        List<String> names = Query.from(people)
            .where(p -> p.age >= 21)                  // Filter adults
            .select(p -> p.name.toUpperCase())        // Map to uppercase names
            .execute();

        names.forEach(System.out::println);          // ALICE, CHARLIE
    }
}

Explanation

Benefits of this DSL Approach

This simple query DSL showcases how Java’s functional features empower developers to create internal DSLs that cleanly express complex operations in an intuitive way.

Index