# Design Patterns in RLM: A Deep Dive into the Recursive Language Model Architecture

> Explore RLM design patterns like Abstract Base Classes, Strategy, Adapter, and Builder. Discover how RLM creates a modular framework for executing LLM code across diverse environments.

- Repository: [az/rlm](https://github.com/alexzhang13/rlm)
- Tags: deep-dive
- Published: 2026-06-18

---

**The RLM (Recursive Language Model) library implements nine core design patterns—including Abstract Base Classes, Strategy, Adapter, and Builder—to create a modular, extensible framework for executing LLM-generated code across diverse environments.**

The alexzhang13/rlm repository provides a Python framework for safely sandboxing and executing code generated by large language models. Understanding the design patterns in RLM reveals how the codebase maintains strict separation between execution environments, LLM provider implementations, and core orchestration logic while remaining highly extensible.

## Environment Architecture Patterns

### Template Method Pattern in BaseEnv

The `BaseEnv` abstract class in [`rlm/environments/base_env.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/environments/base_env.py) (lines 97-104) defines the template for all REPL-like environments. It declares three abstract methods—`setup()`, `load_context()`, and `execute_code()`—that concrete subclasses must implement. This guarantees a consistent public API while allowing vastly different runtimes, from local subprocesses to cloud sandboxes. The hierarchy extends through intermediate classes like `IsolatedEnv` and `NonIsolatedEnv`, but all concrete implementations follow the template established by `BaseEnv`.

### Strategy Pattern for Execution Backends

Concrete environment classes such as `LocalREPL`, `ModalREPL`, `PrimeREPL`, and `DockerREPL` in `rlm/environments/` (see [`local_repl.py`](https://github.com/alexzhang13/rlm/blob/main/local_repl.py) lines 59-79) act as interchangeable strategies. Each implements the `BaseEnv` interface but provides distinct execution logic—local Python subprocess versus isolated Docker container versus remote Modal sandbox. The RLM core selects any environment at runtime without modifying code, adhering to the open/closed principle.

This pattern allows seamless switching between execution strategies:

```python
from rlm import RLM
from rlm.environments import LocalREPL

# Local subprocess strategy

rlm = RLM("gpt-4", env=LocalREPL())
result = rlm.run("Calculate fibonacci(10)")

```

```python
from rlm import RLM
from rlm.environments import ModalREPL

# Cloud-isolated strategy

rlm = RLM("gpt-4", env=ModalREPL(persistent=True))
answer = rlm.run("Write a Python script that scrapes a webpage.")
print(answer.final_answer)

```

### Protocol Interfaces for Optional Capabilities

Python's `Protocol` objects define lightweight interfaces for optional features. In [`rlm/environments/base_env.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/environments/base_env.py) (lines 30-45), `SupportsCustomTools` and `SupportsPersistence` protocols allow the core to query capabilities via `isinstance(env, SupportsCustomTools)`. This decouples feature detection from inheritance hierarchies, enabling duck typing for environments that support custom tool injection or state persistence without forcing all environments to inherit from a heavy base class.

## Object Creation and Configuration

### Factory Method for Environment Registration

The package's [`__init__.py`](https://github.com/alexzhang13/rlm/blob/main/__init__.py) registers concrete environment implementations, exposing them through clean imports like `from rlm.environments import LocalREPL`. This hides concrete class instantiation details from users, allowing the library to evolve by adding new environment types without breaking existing code that relies on the factory interface.

### Builder Pattern for Fluent Configuration

The `RLM` class implements a fluent API that chains configuration calls. As shown in [`examples/quickstart.py`](https://github.com/alexzhang13/rlm/blob/main/examples/quickstart.py) (lines 1-15), users can construct complex instances through method chaining. This builder-style approach makes it easy to assemble fully-specified RLM instances with model, environment, tools, and recursion depth in a readable, step-by-step fashion.

```python
from rlm import RLM
from rlm.environments import LocalREPL

def fetch_data(url: str) -> str:
    return f"data from {url}"

custom_tools = {
    "fetch_data": {"tool": fetch_data, "description": "Downloads JSON from a URL"},
    "API_KEY": "sk-PLACEHOLDER"
}

rlm = (
    RLM(model_name="gpt-4")
    .with_env(LocalREPL(custom_tools=custom_tools))
    .with_depth(2)
)

result = rlm.run(prompt="Use fetch_data to get the weather report.")
print(result.final_answer)

```

## Communication and Coordination Patterns

### Command Pattern for Request Serialization

The `LMRequest` and `LMResponse` data classes in [`rlm/core/comms_utils.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/core/comms_utils.py) (lines 1-30) encapsulate LM calls as command objects. These serializable objects decouple the REPL code from the networking layer, allowing requests to be sent over sockets and handled by `LMHandler`. This pattern makes it straightforward to mock transports or replace communication mechanisms without affecting business logic.

### Observer Pattern for Cloud Sandboxes

Isolated environments like Modal and Prime implement a publish/subscribe model through an HTTP broker. As implemented in [`rlm/environments/modal_repl.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/environments/modal_repl.py) (lines 150-180), the sandbox code POSTs to `/enqueue`, the host polls `/pending`, and the host POSTs results to `/respond`. This observer pattern keeps the sandbox (publisher) and host LM handler (subscriber) loosely coupled while guaranteeing ordered request/response flow across network boundaries.

## Integration and Extension Patterns

### Adapter Pattern for LLM Client Abstraction

The `BaseLM` abstract class in [`rlm/clients/base_lm.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/clients/base_lm.py) (lines 1-25) defines a uniform interface with methods like `completion()`, `acompletion()`, and `get_usage_summary()`. Concrete clients in [`openai.py`](https://github.com/alexzhang13/rlm/blob/main/openai.py), [`anthropic.py`](https://github.com/alexzhang13/rlm/blob/main/anthropic.py), and [`gemini.py`](https://github.com/alexzhang13/rlm/blob/main/gemini.py) adapt provider-specific SDKs to this interface. This adapter pattern isolates the RLM core from vendor API quirks, enabling seamless provider swapping.

```python
from rlm.clients.base_lm import BaseLM
import my_new_vendor_sdk as sdk

class MyVendorClient(BaseLM):
    def __init__(self, api_key: str, model_name: str):
        super().__init__(model_name=model_name)
        self.client = sdk.Client(api_key=api_key)

    def completion(self, prompt, model=None):
        return self.client.generate(prompt, model=model or self.model_name)

# Register the client

from rlm.clients import register_client
register_client("myvendor", MyVendorClient)

```

### Decorator Pattern for Runtime Tool Injection

Custom tool handling in [`rlm/environments/base_env.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/environments/base_env.py) (lines 13-27) uses a decorator-like approach to augment the REPL's global namespace. The `format_tools_for_prompt` and `parse_custom_tools` methods wrap the execution environment, injecting user-provided callables and values while protecting reserved names. This effectively decorates the runtime context without modifying the underlying interpreter.

## Summary

- **Template Method Pattern**: `BaseEnv` defines the skeleton for environment setup, context loading, and code execution (lines 97-104), deferring implementation details to subclasses.
- **Strategy Pattern**: `LocalREPL`, `ModalREPL`, and other environments provide interchangeable execution strategies behind a common interface.
- **Protocol Interfaces**: `SupportsCustomTools` and `SupportsPersistence` (lines 30-45) enable duck-typed capability detection without rigid inheritance.
- **Factory Method**: Environment registration through [`__init__.py`](https://github.com/alexzhang13/rlm/blob/main/__init__.py) abstracts concrete class instantiation.
- **Builder Pattern**: The `RLM` class fluent API enables readable, chained configuration of complex instances.
- **Command Pattern**: `LMRequest` and `LMResponse` (lines 1-30) encapsulate communication as serializable objects.
- **Observer Pattern**: HTTP broker endpoints in [`modal_repl.py`](https://github.com/alexzhang13/rlm/blob/main/modal_repl.py) (lines 150-180) implement publish/subscribe for distributed communication.
- **Adapter Pattern**: `BaseLM` subclasses (lines 1-25) adapt vendor-specific LLM SDKs to a uniform interface.
- **Decorator Pattern**: Runtime tool injection (lines 13-27) augments the REPL namespace without modifying core execution logic.

## Frequently Asked Questions

### What design pattern handles environment selection in RLM?

The **Strategy Pattern** enables environment selection. By implementing a common interface defined in [`rlm/environments/base_env.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/environments/base_env.py), concrete classes like `LocalREPL` and `ModalREPL` become interchangeable. The RLM core treats all environments uniformly, allowing users to switch from local execution to cloud sandboxes by changing a single constructor call without modifying orchestration code.

### How does RLM support multiple LLM providers without vendor lock-in?

The **Adapter Pattern** in [`rlm/clients/base_lm.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/clients/base_lm.py) abstracts provider-specific implementations. Each vendor client (OpenAI, Anthropic, Gemini) inherits from `BaseLM` and adapts its native SDK to the uniform `completion()` and `acompletion()` interface. This allows the rest of the library to remain agnostic to API differences, making it trivial to swap providers or add new ones by implementing the adapter methods.

### What enables the step-by-step configuration of RLM instances?

The **Builder Pattern** powers the fluent API seen in [`examples/quickstart.py`](https://github.com/alexzhang13/rlm/blob/main/examples/quickstart.py). The `RLM` class returns `self` from methods like `with_env()` and `with_custom_tools()`, enabling method chaining. This pattern separates the construction of complex objects from their representation, making configuration readable and preventing partially-initialized instances.

### How do isolated cloud environments communicate with the RLM host?

The **Observer Pattern** facilitates communication between isolated sandboxes (like Modal) and the host. The HTTP broker in [`rlm/environments/modal_repl.py`](https://github.com/alexzhang13/rlm/blob/main/rlm/environments/modal_repl.py) implements a publish/subscribe model where the sandbox POSTs requests to `/enqueue` and the host polls `/pending` for work. This decouples the execution environment from the orchestration layer while maintaining reliable request/response ordering across network boundaries.