Code of Shadows:Mastering Decorator Pattern in Java – Po & Shifu’s Thriller Story of Clean Code

 


The Logging Conspiracy 

It was a cold, misty morning at the Jade Palace. The silence was broken not by combat… but by a mysterious glitch in the logs.

Po (rushing in): "Shifu! The logs… they're missing timestamps!"

Shifu (narrowing his eyes): "This is no accident, Po. This is a breach in the sacred code path. The timekeeper has been silenced."

Traditional OOP Decorator

Shifu unfurled an old Java scroll:

//Interface
package com.javaonfly.designpatterns.decorator.oops;
public interface Loggable {

public void logMessage(String message);
}
//Implementation
package com.javaonfly.designpatterns.decorator.oops.impl;
import com.javaonfly.designpatterns.decorator.oops.Loggable;
public class SimpleLogger implements Loggable {
@Override
public void logMessage(String message) {
System.out.println(message);
}
}
//Implementation
class TimestampLogger implements Loggable {
private Loggable wrapped;

public TimestampLogger(Loggable wrapped) {
this.wrapped = wrapped;
}

public void logMessage(String message) {
String timestamped = "[" + System.currentTimeMillis() + "] " + message;
wrapped.logMessage(timestamped);
}
}

//Calling the decorator
public class Logger {
public static void main(String[] args){
Loggable simpleLogger = new SimpleLogger();
simpleLogger.logMessage("This is a simple log message.");

Loggable timestampedLogger = new TimestampLogger(simpleLogger);
timestampedLogger.logMessage("This is a timestamped log message.");
}
}
//Output
This is a simple log message.
[1748594769477] This is a timestamped log message.

Po: "Wait, we’re creating all these classes just to add a timestamp?"

Shifu: "That is the illusion of control. Each wrapper adds bulk. True elegance lies in Functional Programming."

Functional Decorator Pattern with Lambdas

Shifu waved his staff and rewrote the scroll:

package com.javaonfly.designpatterns.decorator.fp;

import java.time.LocalDateTime;
import java.util.function.Function;

public class Logger {
  //higer order function
    public void decoratedLogMessage(Function<String, String> simpleLogger, Function<String, String> timestampLogger) {
        String message = simpleLogger.andThen(timestampLogger).apply("This is a log message.");
        System.out.println(message);
    }

    public static void main(String[] args){
        Logger logger = new Logger();

        Function<String, String> simpleLogger = message -> {
            System.out.println(message);
            return message;
        };

        Function<String, String> timestampLogger = message -> {
            String timestampedMessage =  "[" + System.currentTimeMillis() + "] " + ": " + message;
            return timestampedMessage;
        };

        logger.decoratedLogMessage(simpleLogger, timestampLogger);
    }
}
//Output
This is a log message.
[1748595357335] This is a log message.

Po (blinking): "So... no more wrappers, just function transformers?"

Shifu (nodding wisely): "Yes, Po. In Functional Programming, functions are first-class citizens. The Function<T, R> interface lets us compose behavior. Each transformation can be chained using andThen, like stacking skills in Kung Fu."

Breaking Down the Code – Functional Wisdom Explained

Po (scratching his head): "Shifu, what exactly is this Function<T, R> thing? Is it some kind of scroll?"

Shifu (gently): "Ah, Po. It is not a scroll. It is a powerful interface from the java.util.function package—a tool forged in the fires of Java 8."

"Function<T, R> represents a function that accepts an input of type T and produces a result of type R."

In our case:

Java
 
Function<String, String> simpleLogger

This means: “Take a String message, and return a modified String message.”

Each logger lambda—like simpleLogger and timestampLogger—does exactly that.

The Art of Composition — andThen

Po (eyes wide): "But how do they all work together? Like… kung fu moves in a combo?"

Shifu (smiling): "Yes. That combo is called composition. And the technique is called andThen."

Java
 
simpleLogger.andThen(timestampLogger)

This means:

  1. First, execute simpleLogger, which prints the message and passes it on.

  2. Then, take the result and pass it to timestampLogger, which adds the timestamp.

This is function chaining—the essence of functional design.

String message = simpleLogger
.andThen(timestampLogger)
.apply("This is a log message.");

Like chaining martial arts techniques, each function passes its result to the next—clean, fluid, precise.

Po: "So the message flows through each function like a river through stones?"

Shifu: "Exactly. That is the way of the Stream."

Functional Flow vs OOP Structure

Shifu (serenely): "Po, unlike the OOP approach where you must wrap one class inside another—creating bulky layers—the functional approach lets you decorate behavior on the fly, without classes or inheritance."

  • No need to create SimpleLoggerTimestampLogger, or interfaces.

  • Just use Function<String, String> lambdas and compose them.

The Secret to Clean Code

“A true master does not add weight to power. He adds precision to purpose.” – Master Shifu

This approach:

  • Eliminates boilerplate.
  • Encourages reusability.
  • Enables testability (each function can be unit-tested in isolation).
  • Supports dynamic behavior chaining.

Po's New Move: Making the Logger Generic

After mastering the basics, Po's eyes sparkled with curiosity.

Po: "Shifu, what if I want this technique to work with any type—not just strings?"

Shifu (with a deep breath): "Yes of course you can ! Try to write it, Dragon warrior."

Po meditated for a moment, and then rewrote the logger:

 public <T> void decoratedLogMessage(Function<T, T>... loggers) {
        Function<T, T> pipeline= Arrays.stream(loggers).sequential().reduce(Function.identity(), Function::andThen);
        T message = pipeline.apply((T) "This is a log message.");
        System.out.println(message);
    }
Po (bowing):
"Master Shifu, after learning to compose logging functions using 
Function<String, String>, I asked myself — what if I could decorate not just strings, but any type of data? Numbers, objects, anything! So I used generics and built this move..."

public <T> void decoratedLogMessage(Function<T, T>... loggers) 
"This declares a generic method where T can be any type — StringInteger, or even a custom User object.
The method takes a 
varargs of Function<T, T> — that means a flexible number of functions that take and return the same type."

Function<T, T> pipeline=
  Arrays.stream(loggers).sequential().reduce(Function.identity(), Function::andThen);
  • "I stream all the logger functions and reduce them into a single pipeline function using Function::andThen.

    • Function.identity() is the neutral starting point — like standing still before striking.

    • Function::andThen chains each logger — like chaining combos in kung fu!"

  • T message = pipeline.apply((T) "This is a log message.");
    

    I apply the final pipeline function to a sample input.
    Since this time I tested it with a String, I cast it as (T). But this method can now accept any type!"

    Shifu (smiling, eyes narrowing with pride):
    "You’ve taken the form beyond its scroll, Po. You have learned not just to use functions—but to respect their essence. This generic version... is the true Dragon Scroll of the Decorator."

    Modified Code by Po

  • package com.javaonfly.designpatterns.decorator.fp;
    
    import java.time.LocalDateTime;
    import java.util.Arrays;
    import java.util.function.Function;
    
    public class Logger {  
        public <T> void decoratedLogMessage(Function<T, T>... loggers) {
            Function<T, T> pipeline= Arrays.stream(loggers).sequential().reduce(Function.identity(), Function::andThen);
            T message = pipeline.apply((T) "This is a log message.");
            System.out.println(message);
        }
    
        public static void main(String[] args){
            Logger logger = new Logger();
            Function<String, String> simpleLogger = message -> {
                System.out.println(message);
                return message;
            };
    
            Function<String, String> timestampLogger = message -> {
                String timestampedMessage =  "[" + System.currentTimeMillis() + "] " + message;
                return timestampedMessage;
            };
            Function<String, String> JadeLogger = message -> {
                String JadeLoggedMessage =  "[jadelog] " + message;
                return JadeLoggedMessage;
            };
       
            logger.decoratedLogMessage(simpleLogger, timestampLogger,JadeLogger);
        }
    }
    //Output
    This is a log message.
    [jadelog] [1748598136677] This is a log message.

  • Wisdom Scroll: OOP vs Functional Decorator

    FeatureOOP DecoratorFunctional Decorator
    Needs ClassYesNo
    Uses InterfaceYesOptional
    ComposabilityRigidElegant
    BoilerplateHighMinimal
    FlexibilityModerateHigh (thanks to lambdas)


Final Words from Master Shifu

"Po, the world of code is full of distractions—designs that look powerful but slow us down. A true Kung Fu developer learns to adapt. To decorate without weight. To enhance without inheritance. To flow with functions, not fight the structure."


Part 1- Kung Fu Code: Master Shifu Teaches Strategy Pattern to Po – the Functional Way!

Kung Fu Code: Master Shifu Teaches Strategy Pattern to Po – the Functional Way!


 "There is no good or bad code . But how you write it… that makes all the difference.”-- Master Shifu


The sun had just touched the tips of the Valley of Peace. Birds chirped, the wind whispered tales of warriors, and Po—the Dragon Warrior—was busy trying to write some Java code. Yes, you read that right.

Master Shifu stood behind him, watching, amused and concerned.

Po (scratching his head): “Master Shifu, I’m trying to make this app where each Kung Fu move is chosen based on the enemy. But the code is… bloated. Classes everywhere. If OOP was noodles, this is a full buffet.”

Shifu (calmly sipping tea): “Ah, the classic Strategy Pattern. But there’s a better way, Po… a functional way. Let me show you the path.”

The Traditional (OOP) Strategy Pattern – Heavy Like Po’s Lunch   

Po wants to choose a fighting strategy based on his opponent.

// Strategy Interface
interface FightStrategy {
void fight();
}

// Concrete Strategies
class TigerFightStrategy implements FightStrategy {
public void fight() {
System.out.println("Attack with swift tiger strikes!");
}
}

class MonkeyFightStrategy implements FightStrategy {
public void fight() {
System.out.println("Use agile monkey flips!");
}
}

// Context
class Warrior {
private FightStrategy strategy;

public Warrior(FightStrategy strategy) {
this.strategy = strategy;
}

public void fight() {
strategy.fight();
}

public void setStrategy(FightStrategy strategy) {
this.strategy = strategy;
}
}

Usage

Warrior po = new Warrior(new TigerFightStrategy());
po.fight(); // Output: Attack with swift tiger strikes!

po.setStrategy(new MonkeyFightStrategy());
po.fight(); // Output: Use agile monkey flips!


Why This Is a Problem (and Why Po Is Annoyed)

Po: “So many files, interfaces, boilerplate! All I want is to change moves easily. This feels like trying to meditate with a noodle cart passing by!”

Indeed, OOP Strategy pattern works, but it's verboserigid, and unnecessarily class-heavy. It violates the spirit of quick Kung Fu adaptability!

Enter Functional Programming – The Way of Inner Simplicity

Shifu (nodding): “Po, what if I told you… that functions themselves can be passed around like scrolls of wisdom?”  

Po: “Whoa... like… JScrolls

Shifu: “No, Po. Java lambdas.” 

In functional programmingfunctions are first-class citizens. You don’t need classes to wrap behavior. You can pass behavior directly.

Higher-Order Functions – functions that take other functions as parameters or return them.

Po, In Java8 onwards , now we can do that easily with the help of lambda, lambda can wrap the functionality and can be pass to another method as a parameter.

Strategy Pattern – The Functional Way in Java



import java.util.function.Consumer;
class Warrior {
private Consumer<Void> strategy;

public Warrior(Consumer<Void> strategy) {
this.strategy = strategy;
}

public void fight() {
strategy.accept(null);
}

public void setStrategy(Consumer<Void> strategy) {
this.strategy = strategy;
}
}

But there’s a better, cleaner way with just lambdas and no class at all.

import java.util.function.Supplier;

public class FunctionalStrategy {

public static void main(String[] args) {
// Each strategy is just a lambda
Runnable tigerStyle = () -> System.out.println("Attack with swift tiger strikes!");
Runnable monkeyStyle = () -> System.out.println("Use agile monkey flips!");
Runnable pandaStyle = () -> System.out.println("Roll and belly-bounce!");

// Fighter is a high-order function executor
executeStrategy(tigerStyle);
executeStrategy(monkeyStyle);
executeStrategy(pandaStyle);
}

static void executeStrategy(Runnable strategy) {
strategy.run();
}
}

Shifu (with a gentle tone):

“Po, in the art of code—as in Kung Fu—not every move needs a name, nor every master a title. In our example, we summoned the ancient scroll of Runnable… a humble interface with but one method—run(). In Java8 , we called it Functional Interface.

Think of it as a silent warrior—it expects no inputs(parameters) , demands no rewards(return type), and yet, performs its duty when called.

Each fighting style—tiger, monkey, panda—was not wrapped in robes of classes, but flowed freely as lambdas.

And then, we had the executeStrategy() method…
a higher-order sensei.

It does not fight itself, Po. It simply receives the wisdom of a move—a function—and executes it when the time is right.

This… is the way of functional composition.
You do not command the move—you invite it.
You do not create many paths—you simply choose the next step.”

Benefits – As Clear As The Sacred Pool of Tears

  • No extra interfaces or classes
  •  Easily switch behaviors at runtime

  • More readable, composable, and flexible

  •  Promotes the power of behavior as data.

Real-World Example: Choosing Payment Strategy in an App

Map<String, Runnable> paymentStrategies = Map.of(
"CARD", () -> System.out.println("Processing via Credit Card"),
"UPI", () -> System.out.println("Processing via UPI"),
"CASH", () -> System.out.println("Processing via Cash")
);

String chosen = "UPI";
paymentStrategies.get(chosen).run(); // Output: Processing via UPI

Po: “This is amazing! It’s like picking dumplings from a basket, but each dumpling is a deadly move.” 

Shifu: “Exactly. The Strategy   was never about the class, Po. It was about choosing the right move at the right moment… effortlessly.” 

One move=One lambda.

The good part is this lambda only holds the move details nothing else. So any warrior can master these moves , to apply the move unnecessary he does not need to reference a bounded object which wrapped this move in a boilerplate class.

Final Words of Wisdom  

“The strength of a great developer lies not in how many patterns they know… but in how effortlessly they flow between object thinking and function weaving to craft code that adapts like water, yet strikes like steel.”-- Master Shifu, on the Tao of Design Patterns.


Coming Up in the Series

Clean code Tips1:: Urge to put comments? refrain to do it.


While you are writing code, assume you are writing a novel and your fellow coders is a reader of your novel, so anytime you think you have to put comment that means something wrong, you cant express yourselves through code. A real clean code does not need comments.


I know it is provocative to put complex business logic in plain English to easily explain the logic,  or Jira id/bug Id as a future reference in the codebase, but with time, the codebase evolves, methods are refactored but we forgot to refactor comments and your comment will be out of sync and gives a wrong interpretation about the method/code/business logic, undoubtedly a code smell,  if you need to put bug id for future reference put as a commit message but avoid comments remember clean code is self-explanatory.

DDD: Thinking in terms of Context Map

In my previous article, I did a detailed discussion about the Bounded Context and learn that how to tackle the complexity of a Domain, it is the best way to divide the domains into several subdomains and mapped them with different bounded contexts where each business entity/Value object has certain meaning on that context, so every stakeholder of the business like Product owner, Developers, Architect, sponsorers are understood the context and referring entity with the proper name, there should be no confusion of the naming when we discuss the terminology basis on a context. A ubiquitous language which creates a unified language among business stakeholder.
By Bounded context, we properly define a business model, create a different context based on the business domains, but always a functionality spans over multiple business entities , and those entities lie in the different Bounded contexts/domains, so it is utmost important to understand the relationship between the Bounded contexts, to architecting the business solution Context Map is a technique by which we can visualize the relationship between different contexts and Integration architect choose the best integration patterns to communicate to other contexts.

Why Context Map is so important while designing solution?
By UML diagram architect cam understand how different parts will communicate with other parts, It gives the architect a view on the communication between different contexts, that is fine but context map stepped in before UML diagram, it helps to visualize the nature of the relationship and based on the nature , architects can decide what kind of technical solution would be adopted.
The best part of Context Map visualization is, it talks about nature of the relationship, It not only tell is that relationship is upstream or downstream, Publisher or subscriber, it also tells how different team dependent on another team their motifs, their politics everything. so counting all of that now architect in a position to decide optimal solution to minimize the risk while integrating with another context.
In the era of Microservices, Context Map is the key player, because before design the holistic Microservice architecture where every team owns a Microservice, It is important to understand how one team is dependent on other, which teams are in commendable position, which team only seeks help then you can architect the solution best possible way .
Think of our Student online Tutor app, as a full grown app say it has more than fifty microservices are deployed in production , so here fifty teams are coming into play and for developing a functionality say Student registrations , multiple contexts will be affected, so we can say to implement that feature multiple teams will be involved so what would be their relationship, while designing this feature who is the pivot service whose data is most needed obviously that service is in commendable position as if that team is not ready other teams can't do anything, so all teams should align with that team , they have to sync there product backlog with that team, so here the internal politics comes to the picture , if the data of the service comes from an External team not with in the organization then the solution is way more complex as you can't force them so only way is to request them and wait for there changes, so based on these diffrent scenarios, politics context map has diffrent solutions. I will cover most important solutions here
 1. Shared Kernel : Shared kernel talks about a partnership relation where two or many teams shared common data model/ value object, it reduces the code duplication as different context use that common model, but that common model/value object is very sensitive, any changes major/minor should be agreed upon all the parties unless it would break other parties code, so more communication and synchronization needs among those teams say one team need a change in the common model but another team is not ready so the priro team has to wait for other oartner to be ready or otherhand othar partner has to change there code unnecessery although that is not heliping them, but to be in sync with other partners, diffrent shades of problems woven while maintaining the pertnership relation ship so choose that pattern  when your common model remains constant or changes once in an era.
In our example, say we developed an analytical module which analysis which courses are most chosen by the students, which students are chosen more than five courses etc, so that module works with Student model, course model so I can say our Analytics module can share Registration modules student model, and they also agreed upon any changes on student model.

Customer/ Supplier: Generally this is the common relationship between two contexts, where a context consumers or depend on data from another context, the context which produces data marked as upstream and the context which consumes data called downstream. When we visualize this relationship in terms of politics we can visualize the power distribution has many shades.
like following
Upstream as the leader: In this type of relationship, the upstream team is in a commendable position, that team does not care about the downstream team, as they producing the data and downstream team is on the mercy of that upstream team they are always changing their model based on the data structure produces by upstream.
In our Student Registration app relationship between Payment app and Notification, app is kind of upstream and downstream where Payment app decide what information in which structure they provide and Notification module consumes that data structure.

Downstream as the Leader: In some cases, the relationship is revert although upstream is produces data, it must have to follow the rule, the data structure for downstream, in this scenario downstream is in a commendable position.
say in our Studen registration system we need to submit Form 16 to government as a tax payee so our payment module has to submit form 16 data to Government exposed API but government API has certain rules and data structure for submitting form 16 data, so although government API is downstream it has total control, our Payment module should communicate with down steam in such a way so it can fulfil downstream rules.

The customer-supplier relation works best when both the parties upstream and downstream are aligned with the work both party agreed upon the interfaces and change in the structure, in case of any changes in the contract both parties will do a discussion synchronize their priority backlogs and agreed upon the changes, If one party does nor care upon another party then every time contract will be broken and it is tough to maintain a customer-supplier relationship.
Conformist: Sometimes, there is a relation between two parties in such a way that downstream team always dependent on an upstream team and they can't do a mutual agreement with upstream about requirements.  The upstream is not aligned with downstream and does not care they are free to change there published endpoint or contract any time and not taking any request from downstream. It is happens when Upstream team is an external system or under a different management hierarchy , and many downstream systems are registered with it so it can't give a priority to any downstream, rather then all downstream system must be aligned with upstream contact and data structures if upstream contracts or data structures change it ps downstream responsibility to changes accordingly.
Say, In our online Student registration app we have a free tutorial module where all students or other application can consume our free tutorials and embed them in their application, so here out free tutorial module acts as upstream and independent of any other third party app who consumes our free tutorials , we can;t give any priority to them and we don't have any contract with them if we change the contracts or data structures it is other third parties duty to change their application accordingly to consume our free tutorials. Other parties are acting as a conformist.

Anti Corruption Layer: When two system interacts if we consume the data directly from upstream we pollute our downstream system as upstream data structure leak through the downstream so if the upstream become polluted our downstream too as it imitates the upstream data while consuming. So it is a good idea while consume data from third party or from a legacy application always use a translation layer where the upstream data translate to downstream data structure before fed in to downstream in that way we can resist the data leakage from upstream, if upstream contract changes it does not pollute downstream internal system only Translation layer has to be changed in order to  adopt new data structure from upstream and convert it into downstream data structure, this technique is called Anti-corruption layer. Anti-corruption layers save the downstream system from upstream changes.
In our application Notification module can implement an ACL while consuming data from payment module so if payment module data structure changes only ACL layers affected.

Open Host : In some cases, your Domain API needs to be accessed by many other services like our Free Tutorial Publisher module, Many external or internal domains want to consume this service, so as Upstream it should be hosted as a service and maintains a protocol and service contract like REST and JSON structure so another system can consume the data.
Published Language: Often two or more system receive and send messages among themselves, in that case, a common language will be needed for the transformation of the data from one system to another like XML, JSON we call that structure as Published language.
The holistic  view of our Student online registration app in terms of context Map

 Conclusion: Context Map is a very important exercise to realize how one domain communicate with other, It gives a proper view of the organization structure, how different domains are distributed, how domain owners are dependent on each other? What is the relationship between team structure? Can they be aligned while developing a feature, based on all the parameters Integration architect can adopt a suitable integration pattern to integrate domains? Prior to designing the integration solution, always architect has to define context map to understand the relationship and structure of the teams based on that Architect can choose the best possible solution.