CrackedRuby CrackedRuby

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