Proxy Design Pattern: Lazy Loading Explained in a Simple Way
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:
ImageProxystores lightweight image information.RealImageperforms 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.