Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Functional Thinking in Java

Posted on December 6, 2018 By Luis Fernandez

I have been writing Java long enough to remember when anonymous classes felt new, and tonight I want to share how I think about functional code in Java without losing readability.

Lambdas landed a while ago and streams are everywhere in code reviews. With Java 11 out this fall and the var keyword from Java 10 already in our muscle memory, it feels like we are carrying a bigger toolbox. At the same time, teams are flirting with Kotlin, flirting with Node on small services, and sprinkling RxJava here and there. I do not see this as a breakup letter to Java. I see it as a nudge to write functional thinking in Java code that still reads clean on a Monday morning.

Functional thinking is not the same as functional syntax. I try to keep three tiny rules in my head. First, push side effects to the edges. Second, pass data in and return data out. Third, keep names that tell the story. That mindset works even if you never type a lambda. When I do reach for lambdas, I remember that clarity beats clever. Short functions with strong names do more for a team than a nested stream that feels like a riddle.

Streams shine when you can describe a flow. Map to transform, filter to prune, and reduce to fold the result. The trap is forcing every loop into a stream. If the clarity falls apart, do not force it. I like to shape code so that every stage has a name. That might mean extracting tiny methods and then using method references. The goal is not fewer lines. The goal is fewer surprises. Below is a small example that compares a classic loop with a stream pipeline that keeps the intent front and center.

There is more to it than streams. Immutability pays for itself when you reason about data flow. Java does not make it free, though we can get close with value style objects, constructors that copy, and builders that do not mutate shared state. Optional is also a nice hint to the reader. It says this value might be missing, deal with it in one place. Add in a small boundary for side effects and the rest of the code can stay calm. If you like the reactive route, RxJava or Reactor give you a richer vocabulary, though the same readability rules apply.

From loop to stream without losing the plot

// Imperative
List<String> emails = new ArrayList<>();
for (User u : users) {
    if (u != null && u.isActive()) {
        String email = u.getEmail();
        if (email != null && email.endsWith("@example.com")) {
            emails.add(email.toLowerCase());
        }
    }
}
// Stream with named steps
List<String> emails =
    users.stream()
         .filter(Objects::nonNull)
         .filter(User::isActive)
         .map(User::getEmail)
         .filter(Objects::nonNull)
         .filter(e -> e.endsWith("@example.com"))
         .map(String::toLowerCase)
         .collect(Collectors.toList());

Both versions work. The stream reads like a story if you keep each step simple. If you feel the urge to squeeze logic into a long lambda, stop and extract a method with a name that fits the domain. Then use a method reference. That one change flips code from look closer to I get it. Your future self will send you a thank you emoji.

Small pure functions make big code calm

// Pure transformation
OrderSummary summarize(Order order) {
    Money total = order.getItems().stream()
        .map(Item::price)
        .reduce(Money.zero(), Money::add);
    return new OrderSummary(order.getId(), total, order.getCreatedAt());
}

The function above does not reach out to the world. It only transforms input into output. That makes it easy to test and easy to reuse. When the only responsibility is the math and the shape of the data, bugs have fewer places to hide. I still keep an eye on collection allocation if this runs hot in production, but the trade is often worth it for clarity, and hot paths can get their own tuned version when profiling proves it is needed.

Optional as a narrative tool

Optional<User> findUser(String id) {
    return Optional.ofNullable(store.get(id));
}

String emailOrFallback(String id) {
    return findUser(id)
        .map(User::getEmail)
        .filter(e -> !e.isEmpty())
        .orElse("nobody@example.com");
}

Optional makes the happy path obvious. The chain tells the reader what happens when data is present and how we fall back when it is not. You could do this with if and it would be fine. The point is not to chase a style badge. The point is to tell the story in fewer branches so brains have more room for the real work.

Make side effects boring on purpose

class BillingService {
    private final PaymentGateway gateway;

    BillingService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    Receipt charge(Order order) {
        OrderSummary summary = summarize(order); // pure
        return gateway.charge(summary.total(), order.getId()); // effect at the edge
    }
}

Push the network call to one place. Keep the rest of the flow pure. Now tests for the messy part stub the gateway and move on. Tests for the pure parts stay fast and expressive. The same pattern works for reading files, publishing events, or writing to a database. A clear border makes refactors less scary when product asks for a new rule on a Friday.

About var and method references

// Java 10 local variable inference
var totalsByUser = orders.stream()
    .collect(Collectors.groupingBy(
        Order::userId,
        Collectors.reducing(Money.zero(), Order::total, Money::add)
    ));

I like var when the type is screaming from the right side. If the expression is long or the type is not obvious, I write it out. Method references carry the same rule. Use them when the name says more than the lambda. Java gives us options and we should treat them as a way to improve readability rather than as a contest to reduce characters.

Teams write the code, not the other way around

Every team has a shared taste. Some prefer simple loops for most tasks. Some lean into streams for every collection flow. What matters is a shared rule set that values naming, small functions, and clear data movement. You can mix object oriented structure with functional thinking and get a codebase that feels stable. The mix is the win. The syntax is a detail.

If you are just getting into lambdas, start with tests. Write tiny pure functions and let the tests tell you if the shape feels right. Move side effects to clear borders and let the rest stay quiet. Read code out loud. If a pipeline sounds muddy, give steps a name or switch to a loop. It is not a downgrade. It is a choice for your future reader, who might be you after three sips of cold coffee.


Functional thinking in Java is less about tricks and more about stories your code can tell without footnotes.

Keep it pure where you can, keep effects at the edges, and keep your names doing the heavy lifting.

Code Development Practices Software Engineering coding-practicesjavaspring

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes