Skip to main content
PProfileMatch

Structural Design Patterns: Adapter, Decorator and Facade in Java

ProgrammingLesson 2 of 47 min read

What you'll learn

  • Tell adapter, decorator and facade apart by intent, not by shape
  • Write an object adapter that translates formats and error codes without adding business logic
  • Compose decorators and predict how their order changes the result
  • Build a facade that hides call ordering and clean-up while staying testable

On this page

  1. Three patterns that look alike and are not
  2. Adapter: translate once, at the border
  3. Decorator: stack behaviour without subclasses
  4. Facade: one front door for a subsystem
  5. Common mistakes
  6. How this shows up in real projects and interviews

Your application defines a clean Locator interface. The vendor's geocoding library, which you may not modify, has a method with a different name, returns coordinates as a semicolon-separated string, and signals errors by returning the text ERR:NOT_FOUND. You could sprinkle string parsing across the codebase, or you could make the vendor's class look like your interface in exactly one place. Structural patterns are the toolkit for that kind of fitting-together.

Three patterns that look alike and are not

Adapter, decorator and facade all involve one object holding another, which is why they are confused. What separates them is the intent and what happens to the interface.

PatternInterface it exposesIntentFamiliar example
AdapterA different one from the wrapped object'sMake an incompatible class usable where your interface is expectedInputStreamReader turning bytes into characters; Arrays.asList presenting an array as a List
DecoratorThe same one as the wrapped object'sAdd behaviour dynamically, and stack severalBufferedReader wrapping another Reader
FacadeA new, simpler one over many objectsHide a subsystem's complexity behind a few methodsA service method that coordinates several repositories and clients

The other structural patterns in the catalogue (proxy, composite, bridge, flyweight) are less common in everyday application code, so this lesson goes deep on these three. All three examples use Java, and each is a single file you can run with java FileName.java.

Adapter: translate once, at the border

An adapter implements the interface your code wants and holds the incompatible object inside. Each call is translated: method names, data formats, and in this case error handling too. The adapter below converts the vendor's error string into an IllegalArgumentException, so nothing downstream ever has to know that ERR: strings exist.

GeoAdapter.java
import java.util.Arrays;

public class GeoAdapter {
    // The interface our application is written against
    interface Locator {
        double[] locate(String address);                  // {latitude, longitude}
    }

    // A vendor class we cannot edit: different method name, string results, error codes
    static class LegacyGeoCoder {
        String lookup(String addressLine) {
            return switch (addressLine.toLowerCase()) {
                case "north depot" -> "10.5;20.25";
                case "east depot" -> "11.75;22.0";
                default -> "ERR:NOT_FOUND";
            };
        }
    }

    // The adapter translates between the two worlds and nothing else
    static class LegacyGeoCoderAdapter implements Locator {
        private final LegacyGeoCoder legacy;

        LegacyGeoCoderAdapter(LegacyGeoCoder legacy) {
            this.legacy = legacy;
        }

        @Override
        public double[] locate(String address) {
            String raw = legacy.lookup(address);
            if (raw.startsWith("ERR:")) {
                throw new IllegalArgumentException("unknown address: " + address);
            }
            String[] parts = raw.split(";");
            return new double[] { Double.parseDouble(parts[0]), Double.parseDouble(parts[1]) };
        }
    }

    public static void main(String[] args) {
        Locator locator = new LegacyGeoCoderAdapter(new LegacyGeoCoder());
        System.out.println(Arrays.toString(locator.locate("North Depot")));
        try {
            locator.locate("Moon base");
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

Output

[10.5, 20.25]
unknown address: Moon base

This is an object adapter: it uses composition, so it can wrap any LegacyGeoCoder instance, including a subclass or a test double. The other form, a class adapter, extends the adaptee and is limited in Java because a class can have only one parent. Keep adapters thin. Their one job is translation; if you catch yourself adding pricing rules or retries, that logic belongs elsewhere. The payoff comes when the vendor changes or you swap providers: you write a second adapter and the rest of the application is untouched.

Decorator: stack behaviour without subclasses

A decorator implements the same interface as the thing it wraps and delegates to it, adding something before or after. Because each decorator is also a LineSource, they can be wrapped around each other in any combination, which solves the subclass explosion described in inheritance versus composition. This is exactly how java.io readers work: you wrap one around another until you get the capabilities you need.

LineDecorators.java
import java.util.Iterator;
import java.util.List;

public class LineDecorators {
    interface LineSource {
        String next();                                    // null when exhausted
    }

    static class ListSource implements LineSource {
        private final Iterator<String> it;

        ListSource(List<String> lines) {
            this.it = lines.iterator();
        }

        public String next() {
            return it.hasNext() ? it.next() : null;
        }
    }

    // Decorators implement the same interface AND wrap one
    static abstract class LineDecorator implements LineSource {
        protected final LineSource inner;

        LineDecorator(LineSource inner) {
            this.inner = inner;
        }
    }

    static class Trimmed extends LineDecorator {
        Trimmed(LineSource inner) {
            super(inner);
        }

        public String next() {
            String line = inner.next();
            return line == null ? null : line.strip();
        }
    }

    static class SkipComments extends LineDecorator {
        SkipComments(LineSource inner) {
            super(inner);
        }

        public String next() {
            String line;
            while ((line = inner.next()) != null) {
                if (!line.isEmpty() && !line.startsWith("#")) {
                    return line;
                }
            }
            return null;
        }
    }

    static class Numbered extends LineDecorator {
        private int count = 0;

        Numbered(LineSource inner) {
            super(inner);
        }

        public String next() {
            String line = inner.next();
            return line == null ? null : (++count) + ": " + line;
        }
    }

    static void drain(LineSource source) {
        String line;
        while ((line = source.next()) != null) {
            System.out.println("  " + line);
        }
    }

    public static void main(String[] args) {
        List<String> raw = List.of("# server list", "  alpha  ", "", "   # retired", "beta");

        System.out.println("trim, then skip comments, then number:");
        drain(new Numbered(new SkipComments(new Trimmed(new ListSource(raw)))));

        System.out.println("skip comments BEFORE trimming:");
        drain(new Numbered(new Trimmed(new SkipComments(new ListSource(raw)))));
    }
}

Output

trim, then skip comments, then number:
  1: alpha
  2: beta
skip comments BEFORE trimming:
  1: alpha
  2: # retired
  3: beta

Compare the two outputs. Same decorators, different order, different result. When trimming comes first, the line # retired becomes # retired and the comment filter catches it. When the comment filter runs first, the leading spaces hide the # and the comment slips through. Order is part of a decorator chain's meaning, so document it or encapsulate the chain in one factory method.

Facade: one front door for a subsystem

A facade offers a simplified interface to a group of classes. It is not about compatibility, as with an adapter, nor about adding behaviour to one object, as with a decorator. It is about *reducing what the caller needs to know*. Onboarding a new hire needs an account, a mailbox and a badge, in that order, with the account removed if a later step fails. Without a facade, each caller must remember the sequence and the clean-up.

OnboardingFacade.java
public class OnboardingFacade {
    static class AccountService {
        String create(String name) {
            System.out.println("account created for " + name);
            return "acc-" + name.toLowerCase();
        }

        void delete(String accountId) {
            System.out.println("account " + accountId + " deleted");
        }
    }

    static class MailboxService {
        void provision(String accountId) {
            System.out.println("mailbox ready for " + accountId);
        }
    }

    static class BadgeService {
        void issue(String accountId, boolean printerOnline) {
            if (!printerOnline) {
                throw new IllegalStateException("badge printer offline");
            }
            System.out.println("badge printed for " + accountId);
        }
    }

    // The facade: one simple method that knows the order and the clean-up rules
    static class Onboarding {
        private final AccountService accounts = new AccountService();
        private final MailboxService mailboxes = new MailboxService();
        private final BadgeService badges = new BadgeService();

        String onboard(String name, boolean printerOnline) {
            String id = accounts.create(name);
            try {
                mailboxes.provision(id);
                badges.issue(id, printerOnline);
                return id;
            } catch (RuntimeException e) {
                accounts.delete(id);                      // callers never need to remember this
                throw e;
            }
        }
    }

    public static void main(String[] args) {
        Onboarding onboarding = new Onboarding();
        System.out.println("result: " + onboarding.onboard("Kavya", true));
        try {
            onboarding.onboard("Rohan", false);
        } catch (IllegalStateException e) {
            System.out.println("failed: " + e.getMessage());
        }
    }
}

Output

account created for Kavya
mailbox ready for acc-kavya
badge printed for acc-kavya
result: acc-kavya
account created for Rohan
mailbox ready for acc-rohan
account acc-rohan deleted
failed: badge printer offline

Onboarding.onboard is now the only thing a caller needs. The failure case in the output shows the value: the badge printer is offline, the facade deletes the half-created account, and the caller sees one exception. A facade does not hide the subsystem classes from people who need them; advanced code can still use AccountService directly. In real code you would also pass the three services into the facade's constructor, rather than constructing them inside as this compact demo does, so a test can substitute fakes.

Common mistakes

  • Business logic in an adapter. Once the adapter applies discounts or retries, you cannot replace the vendor without rewriting rules. Keep translation separate from decisions.
  • Leaking vendor types. If Locator.locate returned LegacyGeoCoder's result object, the adaptation would be pointless. The interface must be expressed in your own terms.
  • Wrong decorator order. The comment slipping through in the second output above is the typical symptom: no exception, just subtly wrong data. Add a test for each supported chain.
  • Forgetting to forward a method. A decorator around a stream that does not forward close() leaves files open. On Windows that appears as "The process cannot access the file because it is being used by another process".
  • A god-facade. A facade with forty methods is a new tangled class, not a simplification. Split it by use case.
  • Swallowing errors in the facade. Catching an exception and returning null makes failures invisible. Clean up, then rethrow, as onboard does.
  • Using a pattern where a method would do. Wrapping a single function call in an adapter class adds files and no flexibility. Wait until there is a second implementation or a clear boundary.

How this shows up in real projects and interviews

Adapters are everywhere in integration work: payment gateways, SMS vendors, legacy databases. Teams routinely write one per provider so that the rest of the code talks to its own interface. Decorators appear as cross-cutting wrappers: logging, caching, retry, metrics, and the middleware chains of web frameworks follow the same idea. Facades are the application-service or use-case classes that hide a pile of repositories and clients behind one method.

Interviews like to ask you to distinguish these three, and to name where they appear in a standard library. Lead with intent: adapter changes the interface, decorator keeps it and adds behaviour, facade simplifies many interfaces into one. Then give the JDK examples from the table. For the patterns that govern *behaviour* rather than structure, continue with behavioral patterns.

Try it yourself

In LineDecorators.java, add an Uppercased decorator and a Limit decorator that stops after N lines (returning null once the limit is reached). Build the chain trim -> skip comments -> uppercase -> limit 1 over the same input and predict the output before running it. Then write the chain in the wrong order and explain in one sentence what changes.

Show a hint

Limit needs a counter field like Numbered. Put Limit last in the chain so that it counts only lines that survive the earlier filters; if you put it first it will count the comment and blank lines too.

Key takeaways

  • Adapter changes an interface to match what your code expects; decorator keeps the interface and adds behaviour; facade offers a simpler interface over several objects.
  • Keep adapters as pure translators, including converting vendor error conventions into your own exceptions.
  • Decorators stack freely because they share one interface, but their order changes the result, so test and document it.
  • A facade centralises call order and clean-up, yet should not hide the subsystem from callers who need it or grow into a god class.
  • Add a wrapper when there is a real boundary or a second implementation, not just to apply a pattern name.

Test yourself

A trick from this lesson's ride: Adapter is a travel plug. Decorator is a phone case. Facade is the reception desk. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

What is the difference between the adapter and decorator pattern?

An adapter changes the interface: it makes an incompatible class fit the interface your code expects. A decorator keeps the same interface and adds behaviour around the wrapped object, and decorators can be stacked. Both wrap another object, but their intent differs.

What is a facade pattern with an example?

A facade is a class that offers a simple interface to a complex subsystem. For example, an Onboarding.onboard(name) method that creates an account, provisions a mailbox and prints a badge in the correct order, rolling back if a step fails, hides three services from the caller.

Where is the decorator pattern used in the Java standard library?

The java.io classes are the textbook case: BufferedReader wraps another Reader, BufferedInputStream wraps an InputStream, and DataInputStream adds typed reads. Each wrapper implements the same abstract type as what it wraps, so they can be stacked.