Structural Design Patterns

Naveen AnamaneniUpdated · 10 min read

Once you have objects, the next question is how to connect them: an old class that doesn't fit your new interface, a feature you want to add without touching existing code, a complicated system you want to hide behind one simple door. Structural patterns are seven proven ways of fitting classes and objects together. Most of them work by wrapping one object inside another, and the difference lies in why you wrap.

The Seven at a Glance

PatternThe problemThe idea
AdapterA class has the wrong shape for the code that wants to use itPut a converter in between
BridgeTwo things vary independently, and subclasses would multiplyKeep them in two separate class families, joined by a field
CompositeGroups and single items should be handled the same wayMake the group implement the same interface as the item
DecoratorYou want to add features without changing a classWrap the object in another that adds the feature
FacadeA system has too many moving parts for callersOffer one simple front door
FlyweightThousands of objects repeat the same dataShare one object for the repeated part
ProxyAccess to an object must be controlled or delayedPut a stand-in in front of it

Adapter

A phone charger from India won't fit a socket in the UK, so you carry a plug adapter. In code, an adapter lets a class with the "wrong" methods be used where a different interface is expected, without changing either side.

AdapterDemo.java▶ Try it
public class AdapterDemo {
    interface PaymentMethod { String pay(long rupees); }
 
    static class OldBankApi {
        String makeTransfer(double amountInPaise) {
            return "Transferred " + (long) amountInPaise + " paise";
        }
    }
 
    static class BankAdapter implements PaymentMethod {
        private final OldBankApi bank = new OldBankApi();
 
        public String pay(long rupees) {
            return bank.makeTransfer(rupees * 100.0);
        }
    }
 
    public static void main(String[] args) {
        PaymentMethod method = new BankAdapter();
        System.out.println(method.pay(250));
    }
}
Output
Transferred 25000 paise

OldBankApi speaks in paise through makeTransfer; the rest of the app speaks in rupees through pay. BankAdapter translates between them, and neither side had to change. Java's Arrays.asList(array) is an adapter that lets an array be used as a List.

Bridge

A TV remote and the TV are separate things. Remotes can be basic or advanced; devices can be TVs or radios. If you made a class for every pair (BasicTvRemote, AdvancedRadioRemote, and so on), the count would multiply with every new device. Bridge keeps remotes and devices in two separate families and connects them with a field.

BridgeDemo.java▶ Try it
public class BridgeDemo {
    interface Device {
        String name();
        int volume();
        void setVolume(int v);
    }
 
    static class Tv implements Device {
        private int volume = 30;
        public String name() { return "TV"; }
        public int volume() { return volume; }
        public void setVolume(int v) { volume = v; }
    }
 
    static class Radio implements Device {
        private int volume = 50;
        public String name() { return "Radio"; }
        public int volume() { return volume; }
        public void setVolume(int v) { volume = v; }
    }
 
    static class Remote {
        protected final Device device;
        Remote(Device device) { this.device = device; }
        void volumeUp() { device.setVolume(device.volume() + 10); }
        String status() { return device.name() + " volume " + device.volume(); }
    }
 
    static class AdvancedRemote extends Remote {
        AdvancedRemote(Device device) { super(device); }
        void mute() { device.setVolume(0); }
    }
 
    public static void main(String[] args) {
        Remote basic = new Remote(new Tv());
        basic.volumeUp();
        System.out.println(basic.status());
 
        AdvancedRemote advanced = new AdvancedRemote(new Radio());
        advanced.volumeUp();
        advanced.mute();
        System.out.println(advanced.status());
    }
}
Output
TV volume 40
Radio volume 0

Any remote works with any device. A new device is one class, and a new kind of remote is one class; nothing multiplies. JDBC works like this: one Java database API, with a separate driver for each database.

Composite

A folder can contain files, and also other folders. Yet when you ask "how big is it?", you want the same question to work for a single file and for a whole folder. Composite makes the group implement the same interface as a single item, so code can treat them alike.

CompositeDemo.java▶ Try it
import java.util.ArrayList;
import java.util.List;
 
public class CompositeDemo {
    interface Item { long size(); }
 
    record FileItem(String name, long size) implements Item { }
 
    static class Folder implements Item {
        private final List<Item> items = new ArrayList<>();
 
        Folder add(Item item) {
            items.add(item);
            return this;
        }
 
        public long size() {
            long total = 0;
            for (Item i : items) {
                total += i.size();
            }
            return total;
        }
    }
 
    public static void main(String[] args) {
        Folder photos = new Folder()
            .add(new FileItem("trip.jpg", 300))
            .add(new FileItem("cake.jpg", 200));
        Folder home = new Folder()
            .add(new FileItem("notes.txt", 5))
            .add(photos);
        System.out.println("Photos: " + photos.size() + " KB");
        System.out.println("Home: " + home.size() + " KB");
    }
}
Output
Photos: 500 KB
Home: 505 KB

Folder.size() simply asks each item for its size, whether that item is a file or another folder. The nesting can go as deep as you like.

Decorator

You order a filter coffee, then ask for milk, then for it to be sugar-free. It's still the same coffee, with extras wrapped around it, and each extra adds to the price. A decorator wraps an object in another object with the same interface that adds one feature.

DecoratorDemo.java▶ Try it
public class DecoratorDemo {
    interface Coffee {
        long cost();
        String description();
    }
 
    static class FilterCoffee implements Coffee {
        public long cost() { return 40; }
        public String description() { return "Filter coffee"; }
    }
 
    static class WithMilk implements Coffee {
        private final Coffee inner;
        WithMilk(Coffee inner) { this.inner = inner; }
        public long cost() { return inner.cost() + 15; }
        public String description() { return inner.description() + ", milk"; }
    }
 
    static class SugarFree implements Coffee {
        private final Coffee inner;
        SugarFree(Coffee inner) { this.inner = inner; }
        public long cost() { return inner.cost() + 10; }
        public String description() {
            return inner.description() + ", sugar-free";
        }
    }
 
    public static void main(String[] args) {
        Coffee order = new SugarFree(new WithMilk(new FilterCoffee()));
        System.out.println(order.description() + ": Rs. " + order.cost());
    }
}
Output
Filter coffee, milk, sugar-free: Rs. 65

Each wrapper calls the coffee inside it and adds its own part. You can combine extras in any order without a class for every combination. Java's I/O classes are built this way: new BufferedReader(new FileReader("notes.txt")) wraps a file reader to add buffering.

Facade

In a hotel, you don't walk to housekeeping, the kitchen and the billing office yourself; you call reception, and reception deals with them. A facade is that front desk for code: one simple method that coordinates several complicated parts.

FacadeDemo.java▶ Try it
public class FacadeDemo {
    static class Inventory {
        boolean reserve(String item) {
            System.out.println("Reserved " + item);
            return true;
        }
    }
 
    static class Payments {
        void charge(String customer, long amount) {
            System.out.println("Charged " + customer + " Rs. " + amount);
        }
    }
 
    static class Shipping {
        void ship(String item, String customer) {
            System.out.println("Shipping " + item + " to " + customer);
        }
    }
 
    static class ShopFacade {
        private final Inventory inventory = new Inventory();
        private final Payments payments = new Payments();
        private final Shipping shipping = new Shipping();
 
        void buy(String customer, String item, long price) {
            if (inventory.reserve(item)) {
                payments.charge(customer, price);
                shipping.ship(item, customer);
            }
        }
    }
 
    public static void main(String[] args) {
        new ShopFacade().buy("Srihitha", "Headphones", 2500);
    }
}
Output
Reserved Headphones
Charged Srihitha Rs. 2500
Shipping Headphones to Srihitha

The caller makes one call, buy, and never needs to know the right order of the three steps. The parts still exist and can be used directly when needed; the facade just makes the common case easy.

Flyweight

A cinema has 500 seats, but only two kinds: Premium and Standard. Storing "Premium, Rs. 350" separately in 100 seat objects wastes memory. Flyweight keeps one shared object per kind, and every seat points to it.

FlyweightDemo.java▶ Try it
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
public class FlyweightDemo {
    record SeatType(String name, long price) { }
 
    record Seat(int row, int number, SeatType type) { }
 
    static class SeatTypes {
        private static final Map<String, SeatType> cache = new HashMap<>();
        static int created = 0;
 
        static SeatType get(String name, long price) {
            SeatType type = cache.get(name);
            if (type == null) {
                type = new SeatType(name, price);
                cache.put(name, type);
                created++;
            }
            return type;
        }
    }
 
    public static void main(String[] args) {
        List<Seat> seats = new ArrayList<>();
        for (int row = 1; row <= 20; row++) {
            for (int n = 1; n <= 25; n++) {
                SeatType type = row <= 4
                    ? SeatTypes.get("Premium", 350)
                    : SeatTypes.get("Standard", 200);
                seats.add(new Seat(row, n, type));
            }
        }
        System.out.println(seats.size() + " seats share "
            + SeatTypes.created + " SeatType objects");
        System.out.println(seats.get(0).type() == seats.get(10).type());
    }
}
Output
500 seats share 2 SeatType objects
true

Each seat still knows its own row and number, but the repeated part (name and price) is one shared object. You've met this idea already: the Integer cache and the string pool share objects the same way (see "Integer Cache in Java").

Proxy

You can't walk straight into a manager's cabin; the receptionist checks who you are first. A proxy stands in front of a real object, with the same interface, and decides whether and when to pass the request on. It's often used to check permissions or to delay creating something expensive until it's really needed.

ProxyDemo.java▶ Try it
public class ProxyDemo {
    interface Report { String open(); }
 
    static class SalaryReport implements Report {
        SalaryReport() {
            System.out.println("Loading salary report from disk");
        }
        public String open() { return "Salary data"; }
    }
 
    static class ReportProxy implements Report {
        private final String user;
        private SalaryReport real;
 
        ReportProxy(String user) { this.user = user; }
 
        public String open() {
            if (!user.equals("Navika")) {
                return "Access denied for " + user;
            }
            if (real == null) {
                real = new SalaryReport();
            }
            return real.open();
        }
    }
 
    public static void main(String[] args) {
        System.out.println(new ReportProxy("Advitha").open());
        Report report = new ReportProxy("Navika");
        System.out.println(report.open());
        System.out.println(report.open());
    }
}
Output
Access denied for Advitha
Loading salary report from disk
Salary data
Salary data

How it works

  1. Advitha's request is refused before the real report is even created.
  2. For Navika, the proxy creates the real report only on the first call (lazy loading), and reuses it after that: "Loading" prints once.
  3. Callers only see the Report interface, so they can't tell a proxy from the real thing.

Common Mistakes

Confusing Decorator and Proxy. A decorator adds behavior the caller wants; a proxy controls access to the same behavior.

A facade that grows into a god class. It should coordinate, not contain all the logic.

Flyweights that can be changed. Shared objects must be immutable, or one seat's change would affect every seat.

Interview Corner

What are the structural design patterns?

They deal with composing classes and objects into larger structures. Adapter converts one interface into another. Bridge separates an abstraction from its implementation so both can vary. Composite treats groups and single objects uniformly. Decorator adds responsibilities by wrapping. Facade provides a simple interface to a complex subsystem. Flyweight shares common state among many objects to save memory. Proxy controls access to another object.

What is the difference between Adapter, Decorator and Proxy?

All three wrap an object. An Adapter changes its interface so it fits code expecting a different one. A Decorator keeps the same interface and adds new behavior. A Proxy keeps the same interface and controls access, for example by checking permissions, loading lazily or calling a remote object.

Practice

  1. Easy: Add a WithChocolate decorator (Rs. 20) to DecoratorDemo.java and order a coffee with milk and chocolate.
  2. Harder: Add a third device, Speaker, to BridgeDemo.java, and use both remotes with it. How many new classes did you need?

Quick check: Structural patterns

4 questions
1A travel plug that lets your charger fit a foreign socket is like:
2new BufferedReader(new FileReader(file)) is an example of:
3Which pattern lets a folder and a file both answer size()?
4Which pattern shares one object among thousands to save memory?

Key Takeaways

Was this tutorial helpful?

Lesson 183 of 482