> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Omnichain Applications (OApps) & Design Patterns

> Omnichain Applications (OApps) are LayerZero-specific contracts with custom business logic for sending and receiving information between chains. OApps use...

**Omnichain Applications (OApps)** are LayerZero-specific contracts with custom business logic for sending and receiving information between chains. OApps use LayerZero's universal interface to implement crosschain coordination through asynchronous messaging patterns.

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
SENDER["Sender OApp"]
ENDPOINT_A["LayerZero<br/>Endpoint"]
end

    subgraph "Chain B"
        RECEIVER_B["Receiver OApp B"]
        ENDPOINT_B["LayerZero<br/>Endpoint"]
    end

    subgraph "Chain C"
        RECEIVER_C["Receiver OApp C"]
        ENDPOINT_C["LayerZero<br/>Endpoint"]
    end

    subgraph "Chain D"
        TARGET_D["Target Contract"]
    end

    SENDER --> ENDPOINT_A

    ENDPOINT_A -->|"Channel 1: Push Messaging"| ENDPOINT_B
    ENDPOINT_A -->|"Channel 2: Push Messaging"| ENDPOINT_C
    ENDPOINT_A -.->|"Channel 3: Pull Messaging"| TARGET_D

    ENDPOINT_B --> RECEIVER_B
    ENDPOINT_C --> RECEIVER_C
    TARGET_D -.->|"Return data"| ENDPOINT_A
```

### Design Pattern Categories

OApp design patterns help teams decide and implement their contract business logic for crosschain coordination:

1. **Architecture Patterns**: How contracts coordinate and interact with one another across multiple chains (hub-spoke or symmetric business logic)
2. **Message Flow Patterns**: How messages travel between chains (one-way push, round-trip ping-pong, batch distribution, compose workflows)
3. **Message Processing Patterns**: How to control message processing (ordered delivery, rate limiting, conditional handling)
4. **Data Access Patterns**: How to retrieve information from other chains (pull messaging, crosschain queries)

**LayerZero's Unopinionated Approach**: LayerZero is unopinionated about your business logic. You can design messaging to be identical on every network, or implement asymmetric coordination patterns. Ultimately, LayerZero is an open framework for you to design any crosschain interaction for any given purpose.

**Key Insight**: These patterns are implementation strategies for your contract business logic, not protocol features. You choose and combine patterns based on your application's specific coordination requirements.

## 1. Architecture Patterns

How contracts coordinate and interact with one another across multiple chains.

### Hub-Spoke Architecture (Asymmetric Coordination)

One chain acts as the central "hub" with coordination logic, while other chains act as "spokes" with execution logic. The hub makes decisions, aggregates data, and distributes commands. Spokes report to the hub and execute received instructions.

```mermaid wrap theme={null}
graph LR
subgraph "Coordination Chain"
HUB[OApp Hub Contract<br/>Coordination Logic]
end

    subgraph "Chain A"
        SPOKE_A[OApp Spoke Contract<br/>Execution Logic]
    end

    subgraph "Chain B"
        SPOKE_B[OApp Spoke Contract<br/>Execution Logic]
    end

    subgraph "Chain C"
        SPOKE_C[OApp Spoke Contract<br/>Execution Logic]
    end

    HUB <-->|"Commands & Reports"| SPOKE_A
    HUB <-->|"Commands & Reports"| SPOKE_B
    HUB <-->|"Commands & Reports"| SPOKE_C
```

<br />

**Use Cases**: Crosschain governance, vault deposits (deep liquidity), oracle aggregation

### Point-to-Point Architecture (Symmetric Business Logic)

All contracts have identical business logic and operate as equal peers. Each contract can initiate communication with any other contract, and all contracts handle messages the same way. No central coordinator - each contract maintains its own state while staying synchronized with peers.

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
PEER_A[OApp Contract<br/>Identical Business Logic]
end

    subgraph "Chain B"
        PEER_B[OApp Contract<br/>Identical Business Logic]
    end

    subgraph "Chain C"
        PEER_C[OApp Contract<br/>Identical Business Logic]
    end

    PEER_A <-->|"Symmetric Messaging"| PEER_B
    PEER_B <-->|"Symmetric Messaging"| PEER_C
    PEER_A <-->|"Symmetric Messaging"| PEER_C
```

<br />

**Use Cases**: Token transfers (OFT), peer-to-peer protocols

## 2. Message Flow Patterns

How messages travel between chains. Push messaging sends data from source to destination chains, with the source initiating and destination processing.

### Batch Send

Send one message to multiple destination chains simultaneously:

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
USER[User]
OAPP_A[OApp A]
end

    subgraph "Chain B"
        OAPP_B[OApp B]
    end

    subgraph "Chain C"
        OAPP_C[OApp C]
    end

    subgraph "Chain D"
        OAPP_D[OApp D]
    end

    USER -->|"Single call"| OAPP_A
    OAPP_A -->|"Message 1"| OAPP_B
    OAPP_A -->|"Message 2"| OAPP_C
    OAPP_A -->|"Message 3"| OAPP_D
```

<br />

**Fee Distribution Logic**: Batch send requires overriding the base OApp fee logic since you're summing multiple quotes into one payment and distributing to each send call:

```solidity wrap theme={null}
// PSEUDOCODE: Batch send with fee distribution

// Override fee check from equivalency to minimum threshold
function _payNative(uint256 _nativeFee) internal override returns (uint256 nativeFee) {
    if (msg.value < _nativeFee) revert NotEnoughNative(msg.value);
    return _nativeFee;
}

function quoteBatch(uint32[] memory dstEids, bytes memory message) public view returns (MessagingFee memory totalFee) {
    // Sum fees for all destinations
    for (uint i = 0; i < dstEids.length; i++) {
        MessagingFee memory fee = _quote(dstEids[i], message, options, false);
        totalFee.nativeFee += fee.nativeFee;
        totalFee.lzTokenFee += fee.lzTokenFee;
    }
}

function sendBatch(uint32[] memory dstEids, bytes memory message) external payable {
    // Validate total fee upfront
    MessagingFee memory totalFee = quoteBatch(dstEids, message);
    require(msg.value >= totalFee.nativeFee, "Insufficient fee");

    // Distribute to each destination
    for (uint i = 0; i < dstEids.length; i++) {
        MessagingFee memory fee = _quote(dstEids[i], message, options, false);
        _lzSend(dstEids[i], message, options, fee, payable(msg.sender));
    }
}
```

**Use Cases**: Configuration updates, price feeds, state synchronization

### Ping-Pong (ABA Pattern)

Chain A sends to Chain B, which calls LayerZero Endpoint within its `_lzReceive` business logic to send back to Chain A:

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
USER[User]
OAPP_A[OApp A]
end

    subgraph "Chain B"
        OAPP_B[OApp B]
        ENDPOINT_B[LayerZero Endpoint]
    end

    USER -->|"Send message"| OAPP_A
    OAPP_A -->|"Crosschain message"| OAPP_B
    OAPP_B -->|"_lzSend() in _lzReceive logic"| ENDPOINT_B
    ENDPOINT_B -->|"Return message"| OAPP_A
```

<br />

**Conditional Message Handling**: The `_lzReceive` business logic can support conditional handling based on message contents. If your application requires it, you can encode conditional identifiers in your message to determine what type of processing should occur:

```solidity wrap theme={null}
// PSEUDOCODE: This pattern organizes your call logic into separate internal functions
// instead of putting all logic directly inside _lzReceive

// Define message type constants
bytes32 constant PUSH = keccak256("PUSH");           // A->B standard message
bytes32 constant PING_PONG = keccak256("PING_PONG"); // A->B->A ping-pong message

function _lzReceive(Origin calldata origin, bytes32 guid, bytes calldata message, address, bytes calldata)
    internal override {
    (bytes32 msgType, bytes memory businessLogic) = abi.decode(message, (bytes32, bytes));

    if (msgType == PING_PONG) {
        _lzReceiveAndReturn(origin, businessLogic);    // Move ping pong logic to separate function
    } else {
        _lzReceiveOnly(businessLogic);                 // Move standard logic to separate function
    }
}

function _lzReceiveAndReturn(Origin calldata origin, bytes memory _message) internal {
    // Process incoming message (your business logic here)
    processMessage(_message);

    // Send response back to source chain (nested _lzSend call)
    bytes memory response = abi.encode(PUSH, generateResponse(_message));
    _lzSend(origin.srcEid, response, options, MessagingFee(0, 0), payable(address(this)));
}

function _lzReceiveOnly(bytes memory payload) internal {
    // Standard message processing without return message (your business logic here)
    processMessage(payload);
}
```

**Critical Gas Consideration**: This pattern requires off-chain gas planning. You must quote the B→A return cost off-chain and include it in your A→B execution options:

```mermaid wrap theme={null}
graph LR
subgraph "Off-Chain Planning"
USER[User]
QUOTE[Quote B→A Cost<br/>Off-chain RPC call]
end

    subgraph "Chain A"
        OAPP_A[OApp A]
    end

    subgraph "Chain B"
        OAPP_B[OApp B]
        ENDPOINT_B[LayerZero Endpoint]
    end

    USER -->|"Get B→A quote"| QUOTE
    QUOTE -->|"Include return cost in options"| USER
    USER -->|"Send A→B with return gas"| OAPP_A
    OAPP_A -->|"Crosschain message<br/>(includes B→A gas)"| OAPP_B
    OAPP_B -->|"_lzSend() with provided gas"| ENDPOINT_B
    ENDPOINT_B -->|"Return message"| OAPP_A
```

<br />

**Key Insight**: The Executor uses the `msg.value` from your `lzReceiveOption` to fund the B→A return message. You must calculate this cost off-chain before sending the initial A→B message.

**Use Cases**: Crosschain authentication, conditional execution, data verification

### Call Composer

Two-step, non-atomic process where the primary message stores a compose message for later execution:

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
USER[User]
OAPP_A[OApp A]
end

    subgraph "Chain B"
        OAPP_B[OApp B]
        ENDPOINT[LayerZero Endpoint<br/>composeMsg storage]
        COMPOSER[Composer Contract]
    end

    USER -->|"Send message"| OAPP_A
    OAPP_A -->|"Crosschain message"| OAPP_B
    OAPP_B -->|"endpoint.sendCompose<br/>(stores by GUID)"| ENDPOINT
    ENDPOINT -.->|"Separate tx<br/>lzCompose call"| COMPOSER
```

<br />

**Key Insight**: The OApp calls `endpoint.sendCompose()` which stores a compose message tied to the LayerZero message GUID. The composer contract is called in a separate transaction, making this a non-atomic, fault-isolated process.

**Use Cases**: Token transfers with automated actions, multi-step DeFi operations

## 3. Message Processing Patterns

How to control message processing.

### Ordered Delivery

OApp enforces strict sequence order by comparing protocol nonce with local nonce tracking:

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
USER[User]
OAPP_A[OApp A]
end

    subgraph "Chain B"
        ENDPOINT_B[LayerZero Endpoint<br/>Protocol nonce tracking]
        OAPP_B[OApp B<br/>Local nonce check]
    end

    USER -->|"Send message"| OAPP_A
    OAPP_A -->|"Crosschain message"| ENDPOINT_B
    ENDPOINT_B -->|"Unordered by default"| OAPP_B
```

<br />

**Key Insight**: The LayerZero Endpoint has its own nonce tracking but delivers messages unordered by default. To implement ordered delivery, the OApp must compare the protocol nonce (from message origin) with its own local nonce tracking and enforce sequence requirements.

**Use Cases**: Financial transactions, workflow dependencies, state machines

### Rate Limiting

Control in-flight capacity per channel over time windows to prevent spam and ensure controlled interactions. Rate limiters track consumed capacity that decays over time.

LayerZero's default rate limiter implementation tracks "in-flight" capacity that decays linearly over time. Unlike simple counters that reset at fixed intervals, this approach provides smooth capacity recovery.

```mermaid wrap theme={null}
graph LR
CONFIG[Per Channel Config<br/>Limit: 100 units<br/>Window: 60 seconds] --> CAPACITY[In-Flight Capacity<br/>Decays linearly over time window]
CAPACITY --> CHECK{Current Capacity +<br/>New Request ≤ Limit?}
CHECK -->|"Yes"| ALLOW[Allow Message<br/>Update in-flight amount]
CHECK -->|"No"| REJECT[Reject Message<br/>Capacity exceeded]
```

<br />

**How It Works**: When a message/token transfer occurs, the rate limiter adds the amount to "in-flight" capacity. This capacity decays linearly over the configured time window. If adding a new request would exceed the limit, the request is rejected. This provides smooth capacity recovery rather than sudden resets.

**Linear Decay Visualization**:

```mermaid wrap theme={null}
graph LR
T0[T=0<br/>100 units<br/>Limit Reached] --> T15[T=15s<br/>75 units<br/>Partial Capacity]
T15 --> T30[T=30s<br/>50 units<br/>Partial Capacity]
T30 --> T45[T=45s<br/>25 units<br/>Partial Capacity]
T45 --> T60[T=60s<br/>0 units<br/>Full Capacity]
```

<br />

**Example**: With a 100-unit limit over 60 seconds, if the limit is reached at T=0, capacity decays at \~1.67 units/second. After 30 seconds, 50 units of capacity are available for new requests. Units can either be the number of individual OApp messages, or a specific value such as amount transferred per channel.

#### Outbound Rate Limiting (Source Chain)

Rate check occurs before sending crosschain message. Clean failure mode with no partial states.

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
USER[User]
OAPP_A[OApp A<br/>Outflow Limiter]
CHECK_A{Within<br/>Capacity?}
REJECT_A[Revert<br/>Transaction]
end

    subgraph "Chain B"
        OAPP_B[OApp B]
    end

    USER -->|"Send message/tokens"| OAPP_A
    OAPP_A --> CHECK_A
    CHECK_A -->|"Yes"| OAPP_B
    CHECK_A -->|"No"| REJECT_A
```

<br />

<Info>
  Transaction fails immediately on source chain - user retains funds, no crosschain state changes.
</Info>

#### Inbound Rate Limiting (Destination Chain)

Rate check occurs after crosschain message arrives. Can create partial states requiring retry handling.

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
USER[User]
OAPP_A[OApp A]
end

    subgraph "Chain B"
        OAPP_B[OApp B<br/>Inflow Limiter]
        CHECK_B{Within<br/>Capacity?}
        REJECT_B[Revert<br/>Transaction]
        PROCESS[Receive<br/>Message]
    end

    USER -->|"Send message"| OAPP_A
    OAPP_A -->|"Crosschain message"| OAPP_B
    OAPP_B --> CHECK_B
    CHECK_B -->|"Yes"| PROCESS
    CHECK_B -->|"No"| REJECT_B
```

<br />

<Warning>
  Transaction succeeds on source but fails on destination - creates partial state where funds may be stuck in-flight. Applications must implement retry UX patterns.
</Warning>

##### Rate Limiter Configuration

**Definition**: Per channel configuration with a limit (number of messages or token value) over a time window. Capacity decays linearly over the window duration, allowing gradual recovery.

**Examples**:

* **Message limiting**: 10 messages per 60 seconds per channel
* **Token limiting**: 1000 USDC per 24 hours per channel
* **Combined limiting**: Both message count and token amount restrictions per channel

**Use Cases**: Spam prevention, regulatory compliance, treasury protection, system stability

## 4. Data Access Patterns

How to retrieve information from other chains. Pull messaging requests data from other chains and returns responses to the requesting chain.

### Data Queries (lzRead)

Request and retrieve state data from contracts on other chains:

```mermaid wrap theme={null}
graph LR
subgraph "Chain A"
USER[User]
OAPP_A[OApp A]
end

    subgraph "Chain B"
        TARGET[Target Contract<br/>State Data]
    end

    USER -->|"Send message"| OAPP_A
    OAPP_A -->|"Crosschain query"| TARGET
    TARGET -->|"Return data"| OAPP_A
```

<br />

**Use Cases**: Price feeds, state verification, crosschain calculations

For comprehensive pull messaging patterns and implementation details, see [Omnichain Queries (lzRead)](/v2/developers/evm/lzread/overview).

## Exit Criteria

Before proceeding to Module 5, you should be able to:

1. Explain what makes an app "omnichain" vs "multi-chain"
2. Identify which pattern fits your use case
3. Design a message schema for your application
4. Implement idempotent message handling

## Further Reading

### Official Documentation

* [OApp Extensions & Design Patterns](/v2/developers/evm/oapp/overview#extensions--design-patterns) - Comprehensive pattern details and advanced examples
* [OApp Quickstart](/v2/developers/evm/oapp/quickstart) - Step-by-step implementation guide

### Code References

* [OApp.sol](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oapp-evm/contracts/oapp/OApp.sol) - Core OApp interface and implementation
* [OptionsBuilder.sol](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol) - Execution options helpers
* [Test Mocks](https://github.com/LayerZero-Labs/devtools/tree/main/packages/test-devtools-evm-foundry/contracts/mocks) - Working pattern implementations

### Related Modules

* Module 3: [LayerZero as Master Interface](./layerzero-protocol-architecture) - The interface OApps build on
* Module 6: [Value Transfer Implementations](./value-transfer-implementations) - Specialized OApp patterns for tokens
