Skip to main content

Command Palette

Search for a command to run...

Proxy Design Pattern: Lazy Loading Explained in a Simple Way

Updated
14 min readView as Markdown

Introduction

The Proxy Design Pattern is a structural design pattern that places an intermediate object between the client and the real object.

Instead of accessing the real object directly, the client communicates with a proxy.

Client → Proxy → Real Object

The proxy controls when and how the real object is accessed.

It can be used for:

  • Lazy loading

  • Security and authorization

  • Caching

  • Logging

  • Remote communication

  • Access control

In this article, we will understand the Proxy Pattern using a simple image lazy-loading example.


What does “Proxy” mean?

A proxy is a representative of another object.

Consider a real-life example involving a company CEO.

A visitor cannot enter the CEO’s office directly. The visitor must first speak with the receptionist.

Visitor → Receptionist → CEO

The receptionist may check:

  • Does the visitor have an appointment?

  • Is the CEO available?

  • What is the purpose of the meeting?

  • Should the visitor be allowed inside?

Here:

Real-world component Proxy Pattern component
Visitor Client
Receptionist Proxy
CEO Real object
Meeting request Method call

The receptionist does not perform the CEO’s actual work. The receptionist only controls access to the CEO.

That is the basic idea behind the Proxy Pattern.


Understanding Lazy Loading

Lazy loading means:

Do not create or load an expensive object until it is actually required.

Suppose we are building an image gallery containing 100 high-quality images.

Loading an image may involve:

  • Downloading it from a server

  • Using internet bandwidth

  • Allocating memory

  • Decoding image pixels

  • Rendering the image on the screen

If the user can see only the first few images, loading all 100 images immediately would waste resources.

Instead, we can initially store only basic information:

File name: shoe.jpg
Image URL: https://server.com/shoe.jpg
Image ID: 101

The actual image can be downloaded when it becomes visible.

This is called lazy loading.


Problem Without Proxy

Consider the following RealImage class:

class RealImage {

    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromServer();
    }

    private void loadFromServer() {
        System.out.println(
                fileName + " is loading from the server"
        );
    }

    public void display() {
        System.out.println(
                fileName + " is displayed"
        );
    }
}

The constructor immediately loads the image:

public RealImage(String fileName) {
    this.fileName = fileName;
    loadFromServer();
}

Now imagine creating three images:

public class Main {

    public static void main(String[] args) {

        RealImage image1 =
                new RealImage("photo1.jpg");

        RealImage image2 =
                new RealImage("photo2.jpg");

        RealImage image3 =
                new RealImage("photo3.jpg");

        image2.display();
    }
}

Output:

photo1.jpg is loading from the server
photo2.jpg is loading from the server
photo3.jpg is loading from the server
photo2.jpg is displayed

Only photo2.jpg was displayed, but all three images were loaded.

This creates several problems:

  • Unnecessary network requests

  • Extra memory consumption

  • Slower application startup

  • Wasted bandwidth

  • Poor user experience

We need a way to store the image information now but load the actual image later.

That is where the Proxy Pattern helps.


Solution Using Proxy Pattern

We will place an ImageProxy between the client and the real image:

Client → ImageProxy → RealImage

The proxy will initially store only the file name.

It will create the RealImage object only when the client calls display().


Step 1: Create a Common Interface

interface Image {
    void display();
}

This interface acts as a common contract for both the real object and the proxy.

Both classes will provide the same method:

display()

Because of this common interface, the client can use the proxy exactly like a real image.


Step 2: Create the Real Object

class RealImage implements Image {

    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromServer();
    }

    private void loadFromServer() {
        System.out.println(
                fileName + " is loading from the server"
        );
    }

    @Override
    public void display() {
        System.out.println(
                fileName + " is displayed"
        );
    }
}

RealImage is responsible for the actual work:

  • Loading the image

  • Storing the real image data

  • Displaying the image

It is called the Real Subject.


Step 3: Create the Proxy

class ImageProxy implements Image {

    private String fileName;
    private RealImage realImage;

    public ImageProxy(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void display() {

        if (realImage == null) {
            realImage = new RealImage(fileName);
        }

        realImage.display();
    }
}

Initially, the proxy stores only:

private String fileName;

The real object is not created immediately:

private RealImage realImage;

Its initial value is:

realImage = null

When display() is called, the proxy checks whether the real object already exists:

if (realImage == null) {
    realImage = new RealImage(fileName);
}

If it does not exist, the proxy creates it.

Finally, the proxy delegates the real work:

realImage.display();

Delegation means:

The proxy receives the request but passes the actual work to the real object.


Complete Java Code

interface Image {
    void display();
}

class RealImage implements Image {

    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromServer();
    }

    private void loadFromServer() {
        System.out.println(
                fileName + " is loading from the server"
        );
    }

    @Override
    public void display() {
        System.out.println(
                fileName + " is displayed"
        );
    }
}

class ImageProxy implements Image {

    private String fileName;
    private RealImage realImage;

    public ImageProxy(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void display() {

        if (realImage == null) {
            realImage = new RealImage(fileName);
        }

        realImage.display();
    }
}

public class Main {

    public static void main(String[] args) {

        Image image1 =
                new ImageProxy("photo1.jpg");

        Image image2 =
                new ImageProxy("photo2.jpg");

        Image image3 =
                new ImageProxy("photo3.jpg");

        System.out.println(
                "All proxy objects have been created"
        );

        image2.display();
    }
}

Output:

All proxy objects have been created
photo2.jpg is loading from the server
photo2.jpg is displayed

Notice that photo1.jpg and photo3.jpg were not loaded because the client never displayed them.


Step-by-Step Execution

Step 1: Proxy objects are created

Image image2 =
        new ImageProxy("photo2.jpg");

At this point, the memory state is:

image2
   ↓
ImageProxy
   fileName = "photo2.jpg"
   realImage = null

The proxy contains only lightweight information.

No server request has been made yet.


Step 2: The client calls display()

image2.display();

The reference type is Image, but the actual object is ImageProxy.

Therefore, Java executes:

ImageProxy.display()

Step 3: The proxy checks the real object

if (realImage == null)

The real image does not exist yet, so the condition is true.


Step 4: The proxy creates the real image

realImage = new RealImage(fileName);

The RealImage constructor executes:

public RealImage(String fileName) {
    this.fileName = fileName;
    loadFromServer();
}

Now the image is loaded from the server.

The memory state becomes:

image2
   ↓
ImageProxy
   fileName = "photo2.jpg"
   realImage ─────────────→ RealImage
                              fileName = "photo2.jpg"

Step 5: The proxy delegates the request

realImage.display();

The actual display operation is performed by RealImage.

ImageProxy receives the request
            ↓
ImageProxy creates RealImage
            ↓
ImageProxy calls RealImage.display()
            ↓
RealImage displays the image

What Happens on the Second Call?

Suppose we call display() twice:

image2.display();
image2.display();

Output:

photo2.jpg is loading from the server
photo2.jpg is displayed
photo2.jpg is displayed

The image is loaded only once.

During the second call:

if (realImage == null)

The condition becomes false because realImage already exists.

Therefore, the proxy reuses the existing object:

realImage.display();

This prevents duplicate loading.


Why Not Create the Real Object Directly at Display Time?

We could write:

new RealImage("photo2.jpg").display();

This is perfectly valid when:

  • There is only one image

  • The image must be displayed immediately

  • The object is not expensive

  • The reference does not need to be stored

  • No access control or caching is required

In such a simple situation, Proxy Pattern is unnecessary.

However, real applications often need to create and store a list of objects before knowing which objects will actually be used.

For example:

List<Image> images = new ArrayList<>();

images.add(new ImageProxy("photo1.jpg"));
images.add(new ImageProxy("photo2.jpg"));
images.add(new ImageProxy("photo3.jpg"));

Later, the client can simply write:

images.get(1).display();

The client does not need to manage object creation manually.

Without a proxy, the client might have to maintain this logic:

if (realImage == null) {
    realImage = new RealImage(fileName);
}

realImage.display();

If this logic is required in many places, it creates duplication and makes the client responsible for managing the real object.

The proxy hides this complexity.

The client only needs to say:

image.display();

The proxy decides:

Does the real object exist?
        ↓
No → Create it and use it
Yes → Reuse the existing object

Where Does This Situation Occur in Real Applications?

1. E-commerce Product Listing

Suppose an e-commerce page contains 100 products.

The application may already know:

  • Product name

  • Product ID

  • Price

  • Image URL

  • Discount

However, only the first few products are visible on the screen.

Products 1–6   → Visible, load their images
Products 7–100 → Not visible, wait for scrolling

The application stores all the image URLs but downloads an image only when its product card approaches the screen.

This improves:

  • Page loading speed

  • Mobile performance

  • Bandwidth usage

  • Memory consumption


2. Social Media Feed

An Instagram-like feed may contain many posts.

The application receives metadata for all posts:

Post ID
Caption
Likes
Image URL
User information

However, only a few posts are visible at one time.

Lazy loading ensures that images are downloaded when the user scrolls near them.


3. PDF Viewer

A PDF may contain 500 pages.

Rendering all 500 pages immediately would be expensive.

A page proxy can initially store only the page number:

Page page = new PageProxy(200);

The actual page is rendered only when the user navigates to page 200:

page.display();

4. Database Relationships

Suppose a customer has 10,000 orders.

When loading the customer, we may only need the customer’s name:

Customer customer =
        customerRepository.findById(10);

customer.getName();

Fetching all 10,000 orders at the same time would be wasteful.

The orders can be loaded when the application calls:

customer.getOrders();

ORM frameworks such as Hibernate can use proxy objects internally for this type of lazy loading.


Roles in the Proxy Pattern

Component Example Responsibility
Subject Image Defines the common contract
Real Subject RealImage Performs the actual expensive work
Proxy ImageProxy Controls access and performs lazy loading
Client Main Uses the object through the interface

Class Relationship

                   Image
                display()
                 /     \
                /       \
       RealImage       ImageProxy
                            |
                            |
                            ↓
                       RealImage

Both RealImage and ImageProxy implement the same Image interface.

The proxy also keeps a reference to RealImage.


Different Types of Proxy

1. Virtual Proxy

A Virtual Proxy delays the creation of an expensive object.

Examples:

  • Image lazy loading

  • PDF page rendering

  • Video thumbnail loading

  • Large document loading

Our image example is a Virtual Proxy.


2. Protection Proxy

A Protection Proxy checks permissions before allowing access.

User → Permission check → Real service

Examples:

  • Admin-only operations

  • Bank PIN verification

  • File permissions

  • Employee role authorization


3. Caching Proxy

A Caching Proxy stores the result of an expensive operation.

Client → Proxy checks cache → Real API

If the result already exists in the cache, the proxy returns it without calling the real service again.


4. Remote Proxy

A Remote Proxy represents an object located on another server.

Application → Remote Proxy → Remote Server

The client uses the proxy like a local object, while the proxy handles network communication.


5. Logging Proxy

A Logging Proxy records information before or after calling the real object.

Client
  ↓
Logging Proxy
  ↓
Real Service

It may record:

  • Method name

  • Request time

  • Response time

  • Errors

  • User information


Proxy Pattern vs Decorator Pattern

Proxy and Decorator may look similar because both wrap another object.

However, their intentions are different.

Proxy Pattern Decorator Pattern
Controls access to an object Adds new behaviour to an object
Used for lazy loading, security and caching Used for combining additional features
May create the real object internally Usually receives the wrapped object
Manages when and how an object is accessed Enhances what the object can do

Example of Proxy:

Can this user access the bank account?

Example of Decorator:

Add milk and sugar to the coffee.

A simple way to remember:

Proxy controls the object. Decorator enhances the object.


Advantages of Proxy Pattern

  • Supports lazy initialization

  • Reduces unnecessary object creation

  • Saves memory and network bandwidth

  • Adds security without changing the real object

  • Can provide caching and logging

  • Keeps additional access logic away from the client

  • The client can use Proxy and Real Object through the same interface


Disadvantages of Proxy Pattern

  • Adds additional classes

  • Makes the execution flow slightly more complex

  • Adds an extra layer between client and real object

  • Incorrect proxy logic may make debugging difficult

  • In some situations, the proxy can introduce a small performance overhead

Therefore, Proxy Pattern should be used when access control or delayed creation provides a real benefit.


When Should We Use Proxy Pattern?

Use Proxy Pattern when:

  • Creating the real object is expensive

  • The real object should be loaded only when required

  • Permission must be checked before access

  • Results should be cached

  • Calls should be logged

  • The real object exists on a remote server

  • The client should not manage object creation directly

Do not use it simply because an object can be wrapped. Use it when you need to control access to the real object.


Interview Explanation

You can explain Proxy Pattern in an interview like this:

Proxy Pattern is a structural design pattern that provides a substitute for a real object. The proxy implements the same interface as the real object and controls access to it. It can perform lazy loading, security checks, caching or logging before delegating the actual operation to the real object.

The most important flow is:

Client → Proxy → Real Object

For lazy loading:

Create lightweight Proxy
          ↓
Store real object as null
          ↓
Client calls display()
          ↓
Proxy checks the real object
          ↓
Create it only when required
          ↓
Delegate the operation

Conclusion

The main purpose of Proxy Pattern is not to replace the real object permanently. Its purpose is to control access to the real object.

In our image example:

  • ImageProxy stores lightweight image information.

  • RealImage performs the expensive loading operation.

  • The real image is created only when display() is called.

  • Once created, the same real object is reused.

The entire concept can be remembered in one sentence:

A Proxy stands between the client and the real object and decides when and how the real object should be accessed.

More from this blog

Amit singh's blog

235 posts