A structural design pattern that provides a surrogate or placeholder object to control access to another object.
Overview
The Proxy Pattern creates an intermediary object that stands in place of another object, controlling access to it. The proxy implements the same interface as the real subject, making it transparent to clients. This indirection layer intercepts operations on the real object, enabling control, optimization, or additional functionality without modifying the original object.
The pattern emerged from the need to manage expensive resources, control access rights, and add functionality without altering core objects. A proxy acts as a gatekeeper, deciding when to forward requests to the real object and when to handle them independently. This separation of concerns maintains clean object responsibilities while adding cross-cutting functionality.
Proxies differ from decorators in intent: decorators add responsibilities to objects, while proxies control access to them. The proxy might delay object creation, restrict operations, or distribute work across systems. The real subject remains unchanged, making proxies ideal for adding access control, lazy initialization, or remote communication without touching existing code.
# Basic proxy concept
class RealSubject
def request
"RealSubject: Handling request"
end
end
class Proxy
def initialize(real_subject)
@real_subject = real_subject
end
def request
return unless check_access
@real_subject.request
end
private
def check_access
puts "Proxy: Checking access before forwarding"
true
end
end
# Client code
subject = RealSubject.new
proxy = Proxy.new(subject)
proxy.request
# => Proxy: Checking access before forwarding
# => RealSubject: Handling request
Key Principles
The Proxy Pattern operates on three fundamental components: the Subject interface, the RealSubject implementation, and the Proxy itself. The Subject defines operations available to clients. The RealSubject performs actual work. The Proxy maintains a reference to the RealSubject and delegates requests to it while adding control logic.
Proxies maintain the same interface as the objects they represent. Clients interact with proxies identically to real objects, unaware of the indirection. This substitutability requires proxies to implement or inherit the subject's interface completely, ensuring behavioral compatibility.
The pattern follows the principle of controlling access rather than adding functionality. While proxies can add behavior, their primary purpose involves managing how and when clients interact with real objects. This control manifests as access restrictions, lazy loading, caching, logging, or remote communication.
Proxy initialization strategies vary by type. Virtual proxies delay creating expensive objects until needed. Protection proxies enforce access control based on caller permissions. Remote proxies represent objects in different address spaces. Smart proxies add reference counting or other resource management.
# Interface compliance demonstration
module Subject
def operation
raise NotImplementedError
end
end
class ConcreteSubject
include Subject
def operation
"ConcreteSubject: Performing operation"
end
end
class AccessProxy
include Subject
def initialize(subject, access_level)
@subject = subject
@access_level = access_level
end
def operation
if @access_level >= 5
@subject.operation
else
"AccessProxy: Access denied"
end
end
end
# Both implement the same interface
subject = ConcreteSubject.new
proxy = AccessProxy.new(subject, 3)
puts proxy.operation
# => AccessProxy: Access denied
admin_proxy = AccessProxy.new(subject, 10)
puts admin_proxy.operation
# => ConcreteSubject: Performing operation
The delegation mechanism forms the pattern's core. The proxy receives requests, performs pre-processing or validation, forwards to the real subject, and potentially post-processes results. This request interception enables the proxy to monitor, restrict, or augment operations without modifying the subject.
Object lifecycle management distinguishes different proxy types. Virtual proxies control when expensive objects come into existence. The proxy starts with a null reference, creates the real object on first access, and caches it for subsequent requests. This lazy initialization reduces memory usage and startup time.
# Lazy initialization in virtual proxy
class ExpensiveObject
def initialize
puts "ExpensiveObject: Initializing (this takes time)"
sleep(2) # Simulating expensive initialization
@data = "Expensive data"
end
def process
"Processing #{@data}"
end
end
class VirtualProxy
def initialize
@real_object = nil
end
def process
@real_object ||= ExpensiveObject.new
@real_object.process
end
end
# Real object not created until first use
proxy = VirtualProxy.new
puts "Proxy created, but real object not initialized yet"
sleep(1)
puts proxy.process # Triggers initialization
puts proxy.process # Uses cached instance
Design Considerations
Selecting between direct object access and proxy-mediated access depends on specific requirements. Use proxies when access control, lazy initialization, resource management, or operation monitoring outweighs the complexity of additional indirection. Direct access suffices when objects are lightweight, security is not a concern, and no special loading behavior is needed.
Proxy types serve distinct purposes. Virtual proxies optimize resource usage by deferring object creation. Protection proxies enforce security policies and access rights. Remote proxies hide network communication details. Smart proxies add reference counting, caching, or logging. Cache proxies store expensive operation results. Choosing the wrong type leads to inappropriate solutions.
The pattern trades simplicity for control. Adding a proxy layer increases code complexity, introduces another point of failure, and adds method call overhead. This cost is acceptable when the benefits—controlled access, deferred initialization, distributed computing—justify the indirection. For simple scenarios, the proxy overhead exceeds its value.
Interface design affects proxy effectiveness. Coarse-grained interfaces with few methods simplify proxy implementation. Fine-grained interfaces with many methods require proxies to implement or forward numerous operations, increasing maintenance burden. Consider interface granularity when planning proxy usage.
# Comparing direct access with proxy-mediated access
class Database
def initialize
puts "Database: Connecting to database server"
@connected = true
end
def query(sql)
raise "Not connected" unless @connected
"Result for: #{sql}"
end
def disconnect
@connected = false
puts "Database: Disconnected"
end
end
class DatabaseProxy
def initialize
@database = nil
@cache = {}
end
def query(sql)
# Lazy initialization
@database ||= Database.new
# Caching layer
@cache[sql] ||= @database.query(sql)
end
def disconnect
@database&.disconnect
end
end
# Direct access: immediate connection
direct = Database.new
# => Database: Connecting to database server
# Proxy access: delayed connection, with caching
proxy = DatabaseProxy.new
puts proxy.query("SELECT * FROM users")
puts proxy.query("SELECT * FROM users") # Cached, no duplicate DB call
Proxy lifecycles require careful management. Stateless proxies are simpler but less capable. Stateful proxies maintain information between calls but increase memory usage and complicate cleanup. Consider whether proxy state persists across requests or resets after each operation.
The pattern interacts with other patterns in predictable ways. Adapter changes interfaces while proxies maintain them. Decorator adds responsibilities while proxies control access. Facade simplifies interfaces while proxies manage access to single objects. Understanding these distinctions prevents pattern misapplication.
Proxies affect object identity and equality semantics. The proxy is not the real object, so identity checks using equal? fail. Equality checks using == require careful implementation. Clients expecting reference equality face issues with proxied objects. Document this behavior to prevent client-side bugs.
Ruby Implementation
Ruby's dynamic nature and metaprogramming capabilities provide multiple proxy implementation approaches. The method_missing mechanism enables proxies to intercept undefined method calls, forwarding them to the real subject. This technique reduces code duplication when proxying objects with many methods.
class LoggingProxy
def initialize(target)
@target = target
end
def method_missing(method, *args, &block)
puts "LoggingProxy: Calling #{method} with args: #{args.inspect}"
start_time = Time.now
result = @target.send(method, *args, &block)
elapsed = Time.now - start_time
puts "LoggingProxy: #{method} completed in #{elapsed}s"
result
end
def respond_to_missing?(method, include_private = false)
@target.respond_to?(method, include_private) || super
end
end
class Calculator
def add(a, b)
a + b
end
def multiply(a, b)
sleep(0.5) # Simulate computation
a * b
end
end
calc = Calculator.new
proxy = LoggingProxy.new(calc)
proxy.add(5, 3)
# => LoggingProxy: Calling add with args: [5, 3]
# => LoggingProxy: add completed in 0.000123s
proxy.multiply(4, 7)
# => LoggingProxy: Calling multiply with args: [4, 7]
# => LoggingProxy: multiply completed in 0.501234s
The SimpleDelegator class from the standard library provides built-in proxy support. It automatically forwards method calls to the wrapped object, simplifying proxy creation when custom forwarding logic is unnecessary. Subclass SimpleDelegator and override specific methods to add proxy behavior while inheriting automatic delegation.
require 'delegate'
class ProtectionProxy < SimpleDelegator
def initialize(obj, allowed_methods)
super(obj)
@allowed_methods = allowed_methods
end
def method_missing(method, *args, &block)
if @allowed_methods.include?(method)
super
else
raise "ProtectionProxy: Method #{method} not allowed"
end
end
def respond_to_missing?(method, include_private = false)
@allowed_methods.include?(method) || super
end
end
class SecureData
def public_info
"This is public"
end
def private_info
"This is private"
end
def admin_info
"This is admin only"
end
end
data = SecureData.new
proxy = ProtectionProxy.new(data, [:public_info, :private_info])
puts proxy.public_info
# => This is public
puts proxy.private_info
# => This is private
begin
proxy.admin_info
rescue => e
puts e.message
# => ProtectionProxy: Method admin_info not allowed
end
The Forwardable module provides declarative method forwarding. Specify which methods to delegate and to which object. This approach makes proxy intentions explicit and avoids method_missing overhead for known methods.
require 'forwardable'
class CachingProxy
extend Forwardable
def initialize(service)
@service = service
@cache = {}
end
# Delegate methods without caching
def_delegator :@service, :status
# Custom implementation with caching
def fetch(key)
@cache[key] ||= begin
puts "CachingProxy: Cache miss for #{key}"
@service.fetch(key)
end
end
def clear_cache
@cache.clear
puts "CachingProxy: Cache cleared"
end
end
class DataService
def fetch(key)
puts "DataService: Fetching #{key} from database"
"Data for #{key}"
end
def status
"Service operational"
end
end
service = DataService.new
proxy = CachingProxy.new(service)
puts proxy.fetch("user:1")
# => CachingProxy: Cache miss for user:1
# => DataService: Fetching user:1 from database
puts proxy.fetch("user:1")
# => Data for user:1 (no cache miss message)
puts proxy.status
# => Service operational
Ruby blocks and procs enable callback-based proxies. Pass blocks to proxy methods to customize behavior at call time, creating flexible proxies that adapt to different contexts without subclassing.
class CallbackProxy
def initialize(target)
@target = target
@before_callbacks = []
@after_callbacks = []
end
def before(&block)
@before_callbacks << block
self
end
def after(&block)
@after_callbacks << block
self
end
def execute(method, *args)
@before_callbacks.each { |cb| cb.call(method, args) }
result = @target.send(method, *args)
@after_callbacks.each { |cb| cb.call(method, args, result) }
result
end
end
class EmailService
def send_email(to, subject)
puts "EmailService: Sending '#{subject}' to #{to}"
"Email sent"
end
end
service = EmailService.new
proxy = CallbackProxy.new(service)
proxy
.before { |method, args| puts "Before: #{method}(#{args.join(', ')})" }
.after { |method, args, result| puts "After: #{method} returned #{result}" }
proxy.execute(:send_email, "user@example.com", "Welcome")
# => Before: send_email(user@example.com, Welcome)
# => EmailService: Sending 'Welcome' to user@example.com
# => After: send_email returned Email sent
Proxy objects integrate with Ruby's object model by responding to introspection methods. Implement respond_to? and respond_to_missing? to maintain accurate method listing. Override is_a? and kind_of? if proxies should masquerade as their subjects.
Common Patterns
Virtual proxies delay expensive object creation until first access. The proxy starts with a null reference, creates the real object on demand, and caches the instance for subsequent operations. This pattern reduces application startup time and memory footprint for infrequently used objects.
class HeavyImage
def initialize(filename)
puts "HeavyImage: Loading #{filename} from disk"
sleep(1) # Simulate expensive loading
@filename = filename
@data = "image data for #{filename}"
end
def display
"Displaying #{@filename}"
end
def dimensions
[1920, 1080]
end
end
class ImageProxy
def initialize(filename)
@filename = filename
@image = nil
end
def display
load_image unless @image
@image.display
end
def dimensions
load_image unless @image
@image.dimensions
end
private
def load_image
@image = HeavyImage.new(@filename)
end
end
# Creating proxies is fast
images = [
ImageProxy.new("photo1.jpg"),
ImageProxy.new("photo2.jpg"),
ImageProxy.new("photo3.jpg")
]
puts "All proxies created"
# Only accessed images are loaded
puts images[0].display
# => HeavyImage: Loading photo1.jpg from disk
# => Displaying photo1.jpg
puts images[0].dimensions
# => [1920, 1080] (no loading message, uses cached instance)
Protection proxies enforce access control based on user permissions, credentials, or application state. The proxy checks authorization before forwarding requests, preventing unauthorized operations. This pattern centralizes security logic, making it easier to audit and modify access rules.
class FileSystem
def read(path)
"Contents of #{path}"
end
def write(path, data)
"Wrote to #{path}"
end
def delete(path)
"Deleted #{path}"
end
end
class FileSystemProxy
PERMISSIONS = {
guest: [:read],
user: [:read, :write],
admin: [:read, :write, :delete]
}
def initialize(filesystem, role)
@filesystem = filesystem
@role = role
end
def read(path)
check_permission(:read)
@filesystem.read(path)
end
def write(path, data)
check_permission(:write)
@filesystem.write(path, data)
end
def delete(path)
check_permission(:delete)
@filesystem.delete(path)
end
private
def check_permission(operation)
allowed = PERMISSIONS[@role] || []
raise "Permission denied for #{operation}" unless allowed.include?(operation)
end
end
fs = FileSystem.new
guest_fs = FileSystemProxy.new(fs, :guest)
puts guest_fs.read("/public/data.txt")
# => Contents of /public/data.txt
begin
guest_fs.write("/public/data.txt", "new content")
rescue => e
puts e.message
# => Permission denied for write
end
admin_fs = FileSystemProxy.new(fs, :admin)
puts admin_fs.delete("/temp/old.txt")
# => Deleted /temp/old.txt
Remote proxies represent objects in different address spaces, hiding network communication complexity. The proxy serializes method arguments, transmits them over the network, deserializes responses, and returns results to clients. This pattern makes distributed systems appear local.
require 'json'
class RemoteService
def calculate(operation, a, b)
case operation
when :add then a + b
when :multiply then a * b
else raise "Unknown operation"
end
end
end
class RemoteServiceProxy
def initialize(host, port)
@host = host
@port = port
end
def calculate(operation, a, b)
# Simulate network request
request = { method: 'calculate', args: [operation, a, b] }.to_json
puts "RemoteServiceProxy: Sending request to #{@host}:#{@port}"
puts "Request: #{request}"
# Simulate network call and response
sleep(0.1)
response = simulate_remote_call(request)
puts "Response: #{response}"
JSON.parse(response)['result']
end
private
def simulate_remote_call(request)
# In reality, this would use HTTP, RPC, or other network protocol
data = JSON.parse(request)
service = RemoteService.new
result = service.send(data['method'], *data['args'])
{ result: result }.to_json
end
end
proxy = RemoteServiceProxy.new("api.example.com", 8080)
puts proxy.calculate(:add, 10, 5)
# => RemoteServiceProxy: Sending request to api.example.com:8080
# => Request: {"method":"calculate","args":["add",10,5]}
# => Response: {"result":15}
# => 15
Smart proxies add reference counting, locking, or other resource management. They track object usage, manage shared resources, or implement copy-on-write semantics. These proxies handle cross-cutting concerns that don't belong in domain objects.
class SharedResource
def initialize(name)
@name = name
puts "SharedResource: Created #{@name}"
end
def use
puts "SharedResource: Using #{@name}"
"Result from #{@name}"
end
def cleanup
puts "SharedResource: Cleaning up #{@name}"
end
end
class SmartProxy
def initialize(resource_name)
@resource_name = resource_name
@resource = nil
@reference_count = 0
end
def acquire
@reference_count += 1
@resource ||= SharedResource.new(@resource_name)
puts "SmartProxy: Reference count: #{@reference_count}"
self
end
def use
raise "Must acquire before use" if @reference_count == 0
@resource.use
end
def release
return if @reference_count == 0
@reference_count -= 1
puts "SmartProxy: Reference count: #{@reference_count}"
if @reference_count == 0
@resource.cleanup
@resource = nil
end
end
end
proxy = SmartProxy.new("Database Connection")
proxy.acquire.use
# => SharedResource: Created Database Connection
# => SmartProxy: Reference count: 1
# => SharedResource: Using Database Connection
proxy.acquire.use # Another client
# => SmartProxy: Reference count: 2
# => SharedResource: Using Database Connection
proxy.release
# => SmartProxy: Reference count: 1
proxy.release
# => SmartProxy: Reference count: 0
# => SharedResource: Cleaning up Database Connection
Practical Examples
A lazy-loading document viewer demonstrates virtual proxy usage. Documents load only when accessed, reducing initial memory consumption. The proxy tracks loading state and forwards operations to the real document once loaded.
class Document
def initialize(title, size_mb)
@title = title
@size_mb = size_mb
puts "Document: Loading '#{@title}' (#{@size_mb}MB)"
sleep(@size_mb * 0.1) # Simulate loading time
@content = "Content of #{@title}" * 100
end
def title
@title
end
def preview
@content[0..50] + "..."
end
def full_content
@content
end
def word_count
@content.split.size
end
end
class DocumentProxy
def initialize(title, size_mb)
@title = title
@size_mb = size_mb
@document = nil
end
# Lightweight operation, no loading needed
def title
@title
end
# Heavy operations trigger loading
def preview
ensure_loaded
@document.preview
end
def full_content
ensure_loaded
@document.full_content
end
def word_count
ensure_loaded
@document.word_count
end
private
def ensure_loaded
@document ||= Document.new(@title, @size_mb)
end
end
# Create document proxies quickly
docs = [
DocumentProxy.new("Report Q1", 50),
DocumentProxy.new("Report Q2", 45),
DocumentProxy.new("Report Q3", 55)
]
puts "All documents registered (not loaded yet)"
puts "\nAccessing titles (fast):"
docs.each { |doc| puts doc.title }
puts "\nAccessing first document content (triggers loading):"
puts docs[0].preview
# => Document: Loading 'Report Q1' (50MB)
# => Content of Report Q1Content of Report Q1Content...
puts "\nAccessing same document again (already loaded):"
puts docs[0].word_count
# => 200 (no loading message)
An API rate limiter uses protection proxy to enforce request quotas. The proxy tracks request counts, denies requests exceeding limits, and resets counts after time windows expire.
class APIClient
def get(endpoint)
"GET response from #{endpoint}"
end
def post(endpoint, data)
"POST response from #{endpoint} with #{data}"
end
end
class RateLimitProxy
def initialize(client, requests_per_minute)
@client = client
@limit = requests_per_minute
@requests = []
end
def get(endpoint)
check_rate_limit
@client.get(endpoint)
end
def post(endpoint, data)
check_rate_limit
@client.post(endpoint, data)
end
private
def check_rate_limit
now = Time.now
one_minute_ago = now - 60
# Remove requests older than 1 minute
@requests.reject! { |time| time < one_minute_ago }
if @requests.size >= @limit
oldest = @requests.first
wait_time = 60 - (now - oldest)
raise "Rate limit exceeded. Retry in #{wait_time.ceil} seconds"
end
@requests << now
end
end
client = APIClient.new
proxy = RateLimitProxy.new(client, 3)
puts proxy.get("/users")
puts proxy.get("/posts")
puts proxy.get("/comments")
begin
puts proxy.get("/likes")
rescue => e
puts "Error: #{e.message}"
# => Error: Rate limit exceeded. Retry in 60 seconds
end
A database connection pool uses smart proxy to manage shared connections. The proxy tracks active connections, enforces maximum pool size, and handles connection reuse.
class DatabaseConnection
def initialize(id)
@id = id
@in_use = false
puts "DatabaseConnection: Opened connection #{@id}"
end
def query(sql)
raise "Connection not in use" unless @in_use
"Result for: #{sql}"
end
def mark_in_use
@in_use = true
end
def mark_available
@in_use = false
end
def in_use?
@in_use
end
def close
puts "DatabaseConnection: Closed connection #{@id}"
end
end
class ConnectionPoolProxy
def initialize(max_connections)
@max_connections = max_connections
@connections = []
@available_connections = []
end
def with_connection
connection = acquire_connection
begin
yield connection
ensure
release_connection(connection)
end
end
def stats
{
total: @connections.size,
available: @available_connections.size,
in_use: @connections.count(&:in_use?)
}
end
private
def acquire_connection
connection = @available_connections.pop
if connection.nil? && @connections.size < @max_connections
connection = DatabaseConnection.new(@connections.size + 1)
@connections << connection
end
raise "No connections available" if connection.nil?
connection.mark_in_use
connection
end
def release_connection(connection)
connection.mark_available
@available_connections << connection
end
end
pool = ConnectionPoolProxy.new(2)
# Execute queries using pool
pool.with_connection do |conn|
puts conn.query("SELECT * FROM users")
end
pool.with_connection do |conn|
puts conn.query("SELECT * FROM posts")
end
puts "Pool stats: #{pool.stats}"
# => Pool stats: {:total=>1, :available=>1, :in_use=>0}
# Exceed pool size
pool.with_connection do |conn1|
pool.with_connection do |conn2|
puts "Both connections in use"
puts "Pool stats: #{pool.stats}"
# => Pool stats: {:total=>2, :available=>0, :in_use=>2}
begin
pool.with_connection { |conn3| } # Would need 3rd connection
rescue => e
puts "Error: #{e.message}"
# => Error: No connections available
end
end
end
Performance Considerations
Proxies introduce method call overhead. Each proxied method requires at least one additional method invocation. For frequently called methods on hot code paths, this overhead accumulates. Measure performance impact before introducing proxies in performance-critical sections.
Virtual proxies improve startup performance and memory usage when deferring expensive object creation. The cost comes from checking whether the real object exists on each method call. Minimize this overhead by caching the loaded state or using conditional checks only for heavy operations.
require 'benchmark'
class ExpensiveComputation
def initialize
sleep(0.5) # Expensive initialization
@data = (1..1000).to_a
end
def compute
@data.sum
end
end
class LazyProxy
def initialize
@real = nil
end
def compute
@real ||= ExpensiveComputation.new
@real.compute
end
end
# Compare direct instantiation vs proxy
Benchmark.bm(20) do |x|
x.report("Direct (100 calls):") do
obj = ExpensiveComputation.new
100.times { obj.compute }
end
x.report("Proxy (100 calls):") do
proxy = LazyProxy.new
100.times { proxy.compute }
end
end
# Direct: ~0.5s (one initialization)
# Proxy: ~0.5s + minimal overhead (one lazy initialization)
Caching proxies trade memory for speed. Storing operation results reduces repeated computation or I/O. Cache size management prevents unbounded memory growth. Implement cache eviction policies like LRU when memory constraints matter.
Protection proxies add security check overhead. Minimize this by performing cheap checks first, caching authorization results when appropriate, and avoiding complex permission calculations on every call. Consider authorization middleware or decorators for high-throughput systems.
Remote proxies incur significant network latency. Batch multiple operations when possible. Implement request queuing to reduce round trips. Use asynchronous operations to prevent blocking. Cache remote data aggressively to minimize network calls.
Method forwarding approaches have different performance characteristics. method_missing is slower than explicit method definitions because it involves dynamic method lookup. SimpleDelegator offers better performance than method_missing for known methods. Explicit delegation with Forwardable provides the fastest performance but requires more code.
Common Pitfalls
Object identity breaks with proxies because proxies are different objects than their subjects. Code comparing object identity using equal? or object_id fails when proxies are involved. Use equality comparison methods that check values rather than identity.
class User
attr_reader :id, :name
def initialize(id, name)
@id = id
@name = name
end
def ==(other)
other.is_a?(User) && @id == other.id
end
end
class UserProxy < SimpleDelegator
def initialize(user)
super(user)
end
end
user = User.new(1, "Alice")
proxy = UserProxy.new(user)
# Identity check fails
puts user.equal?(proxy)
# => false
# Value equality works if implemented
puts user == proxy
# => true (if proxy delegates ==)
# Hash keys fail with identity
cache = { user => "data" }
puts cache[proxy]
# => nil (different object_id)
Method interception gaps occur when proxies fail to forward all methods. Ruby objects have many methods from Object and Kernel. Proxies must decide which to intercept and which to handle directly. Forgetting to proxy methods like to_s, inspect, or respond_to? causes unexpected behavior.
class IncompleteProxy
def initialize(target)
@target = target
end
def method_missing(method, *args, &block)
@target.send(method, *args, &block)
end
# Missing: respond_to_missing?
# Missing: custom to_s/inspect
end
class Service
def process
"processing"
end
end
service = Service.new
proxy = IncompleteProxy.new(service)
# Works
puts proxy.process
# => processing
# Broken: respond_to? doesn't check forwarding
puts proxy.respond_to?(:process)
# => false (should be true)
# Broken: inspect shows proxy object, not target
puts proxy.inspect
# => #<IncompleteProxy:0x...> (not helpful)
Circular references create memory leaks and stack overflow. If a proxy references an object that references the proxy, garbage collection cannot reclaim memory. Avoid storing proxies in proxied objects or maintain weak references.
Initialization order matters with lazy proxies. If the real object's constructor requires arguments only available later, the proxy must store these arguments until initialization. Missing parameters cause runtime errors when the real object finally loads.
class LazyService
def initialize
@service = nil
@config = nil
end
def configure(config)
@config = config
self
end
def call(input)
# BUG: @config might be nil
@service ||= RealService.new(@config)
@service.call(input)
end
end
# Correct usage
proxy = LazyService.new
proxy.configure(settings)
proxy.call(data)
# Bug: forgetting configure
proxy2 = LazyService.new
proxy2.call(data) # Crash: @config is nil
State synchronization challenges emerge with stateful proxies. If the proxy maintains its own state separate from the real object, keeping them synchronized requires explicit management. Changes to the real object bypass proxy state tracking.
Thread safety issues appear when proxies are shared across threads. Without proper synchronization, concurrent access to lazy-loaded objects, reference counts, or caches causes race conditions. Use mutexes or thread-local storage for thread-safe proxies.
Reference
Proxy Types
| Type | Purpose | Use Case |
|---|---|---|
| Virtual Proxy | Delays expensive object creation | Large images, database connections |
| Protection Proxy | Controls access based on permissions | Security, authentication, authorization |
| Remote Proxy | Represents remote objects | Distributed systems, API clients |
| Smart Proxy | Adds resource management | Reference counting, locking, logging |
| Cache Proxy | Stores operation results | Expensive computations, API calls |
Ruby Implementation Techniques
| Technique | Advantages | Disadvantages |
|---|---|---|
| method_missing | Minimal code, handles all methods | Slower, breaks respond_to? |
| SimpleDelegator | Automatic forwarding, fast | Less control over forwarding |
| Forwardable | Explicit, fastest | Verbose, manual method listing |
| Inheritance | Full control | Tight coupling, fragile |
Decision Matrix
| Requirement | Recommended Approach |
|---|---|
| Defer expensive initialization | Virtual Proxy with lazy loading |
| Enforce access control | Protection Proxy with permission checks |
| Represent network resource | Remote Proxy with serialization |
| Track resource usage | Smart Proxy with reference counting |
| Reduce repeated operations | Cache Proxy with result storage |
| Log method calls | Logging Proxy with method_missing |
| Many methods to forward | SimpleDelegator or method_missing |
| Few methods to forward | Explicit delegation with Forwardable |
Key Methods to Implement
| Method | Purpose | Critical For |
|---|---|---|
| method_missing | Intercept undefined methods | Dynamic forwarding |
| respond_to_missing? | Fix method introspection | Proper respond_to? behavior |
| getobj | Access wrapped object | SimpleDelegator subclasses |
| setobj | Change wrapped object | Dynamic proxy targets |
| inspect | Provide meaningful output | Debugging |
| to_s | String representation | Logging, output |
Common Patterns
| Pattern | Implementation | When to Use |
|---|---|---|
| Lazy Load | Check nil, create on first access | Expensive initialization |
| Access Check | Validate before forwarding | Security requirements |
| Caching | Store results in hash | Repeated operations |
| Logging | Wrap calls with log statements | Debugging, monitoring |
| Reference Counting | Increment/decrement counter | Shared resource management |
Performance Characteristics
| Operation | Time Complexity | Notes |
|---|---|---|
| Proxy creation | O(1) | Fast, no real object created |
| First access (lazy) | O(n) | Creates real object |
| Subsequent access | O(1) + overhead | Method forwarding overhead |
| Cache lookup | O(1) | Hash-based caching |
| Permission check | Varies | Depends on check complexity |
Integration Points
| System Component | Integration Approach |
|---|---|
| ORM Models | Lazy load associations |
| Authentication | Protection proxy on services |
| API Clients | Remote proxy for endpoints |
| File Systems | Virtual proxy for large files |
| Thread Pools | Smart proxy for connection management |
| Caching Layer | Cache proxy for expensive operations |