Broker Adapter Architecture

Direct Answer

A broker adapter is a software component that sits between your strategy logic and a specific broker or exchange API. Its job is to accept normalized order commands in a venue-agnostic format and translate them into the exact wire-format messages, authentication headers, and field values the target venue requires, then parse the venue's responses back into the same normalized format. The strategy never knows which broker it's talking to. This decoupling means you can replace a broker, run the same strategy on multiple venues simultaneously, or A/B test execution quality without touching strategy code.

The adapter pattern applied to broker integration is a direct application of the Gang-of-Four Adapter structural pattern: define a target interface your application depends on, then write one concrete adapter class per venue that implements that interface using the venue's actual SDK or HTTP client. The interface becomes the contract; each adapter is an isolated, testable translation unit.

Key Takeaways

  • Define the interface first: Before writing any adapter, specify the canonical order model your system will use internally, field names, enum values, and semantics, then write adapters that translate to and from it.
  • One adapter per venue: Each broker gets its own adapter class. Shared logic (retry, logging, rate limiting) belongs in a base class or middleware layer, not duplicated across adapters.
  • Adapters own authentication: Token refresh, API key rotation, and credential management belong in the adapter, not scattered across strategy code.
  • Model broker events, not just responses: Order fills, cancellations, and position updates often arrive asynchronously via WebSocket or webhooks. The adapter must normalize these event streams alongside REST responses.
  • Capability flags are part of the interface: Each adapter should expose a capability manifest, which order types, TIF values, and asset classes it supports, so the routing layer can make informed decisions without trying the request.
  • Idempotency keys prevent duplicate orders: Every order submission through the adapter should carry a client-generated order ID that the broker can use to deduplicate retries, preventing double-fills on timeout-then-retry scenarios.
  • Keep adapters thin: Business logic, position sizing, risk checks, signal generation, must not live inside the adapter. Adapters translate; they don't decide.
  • Test adapters in isolation: Because adapters contain translation logic, they are the ideal layer for contract tests: assert that a given internal order model produces the expected wire-format JSON, and that a given broker response JSON parses to the expected internal model.

Core Concepts

The Canonical Order Model

The canonical order model is the data structure your entire system speaks internally. It defines a venue-agnostic vocabulary: symbol (not "ticker" or "instrument"), side ("buy" | "sell", not "B" or "1"), orderType ("limit" | "market" | "stop_limit"), quantity, limitPrice, timeInForce ("day" | "gtc" | "ioc" | "fok"), and clientOrderId. Every strategy emits objects conforming to this schema.

The adapter's outbound translation function accepts a canonical order object and produces a broker-specific payload. For Interactive Brokers, orderType: "limit" becomes the string "LMT" in the orderType field. For Alpaca, it becomes "limit". For TD Ameritrade's old API, it became the integer 0. These differences are entirely contained in the adapter; nothing else needs to know.

The canonical fill event mirrors this: a broker-specific execution report, Alpaca's trade_updates message, IBKR's execDetails callback, Coinbase's match message, all normalize to the same internal fill object: { orderId, symbol, side, quantity, price, timestamp, fees }. Downstream position tracking, P&L calculation, and reporting all consume this normalized event regardless of venue.

To test the canonical model, write serialization round-trip tests: create a canonical order, serialize it to broker A's format, deserialize it back, and assert the result equals the original. Do this for every field, including edge cases like fractional quantities and extended-hours flags.

The Adapter Interface

The adapter interface is a formal contract that every concrete adapter must fulfill. In typed languages. This is a class interface or protocol; in dynamic languages, it's an enforced duck-type. A minimal interface includes: submitOrder(order: CanonicalOrder): Promise<OrderAck>, cancelOrder(orderId: string): Promise<void>, getOrderStatus(orderId: string): Promise<OrderStatus>, getPositions(): Promise<Position[]>, getAccountState(): Promise<AccountState>, and subscribeToOrderUpdates(callback): void.

In practice, this interface expands to include capability querying: getCapabilities(): BrokerCapabilityManifest. The manifest lists supported order types, supported TIF values per order type, supported asset classes, whether fractional shares are available, minimum order sizes, and rate limit parameters. The routing layer consults this manifest before routing an order, rather than discovering limitations through rejection.

The interface should also define how adapters report their own health. A getStatus(): AdapterStatus method returning connected | degraded | disconnected enables a circuit breaker upstream to make failover decisions without guessing from error logs.

Verify that your interface is genuinely sufficient by writing mock adapters, in-memory implementations that simulate fills, and running your entire strategy stack against them. If the mock adapter reveals that the interface is missing a method the strategy needs, you've found the gap before writing real broker code.

Middleware and Cross-Cutting Concerns

Retry logic, rate limiting, request logging, and latency measurement are cross-cutting concerns that belong in a middleware layer wrapping each adapter, not inside the adapter itself. The most common pattern is a decorator: a RetryingAdapter wraps any IBrokerAdapter and adds retry logic to every method call. A RateLimitedAdapter wraps that, enforcing per-endpoint limits.

Rate limiting at the adapter middleware layer requires per-endpoint counters, not a single global counter. Interactive Brokers, for example, enforces limits per message type: order submissions have a different limit than market data requests. Alpaca enforces limits per 60-second window with a burst allowance. Each adapter's middleware configuration should be initialized with the venue's specific limit parameters.

Logging at the middleware layer should record every outbound request and inbound response, including headers and timestamps. Timestamp both the request emission and the response receipt, the difference is your observed network latency for that call. Over time, this data reveals latency degradation that often precedes a venue's announced maintenance window.

Circuit breakers belong here too. After N consecutive failures within a window, the circuit opens: all subsequent calls return immediately with a circuit-open error rather than attempting the network call. This prevents a struggling broker API from consuming all available retry budget and delaying the failover decision.

Handling Asynchronous Events

Most brokers deliver order status updates asynchronously rather than in the REST response to the order submission. WebSocket messages, fix protocol messages, and webhook POST callbacks all represent the broker pushing an event to you. The adapter must normalize these push events into the same event stream that REST polling would produce.

The event normalization pattern: the adapter maintains an internal event emitter or observable. Regardless of whether an update arrives via WebSocket push or REST poll, it gets normalized and emitted through the same internal channel. Subscribers, position managers, risk monitors, strategy logic, subscribe once and receive all events without knowing their transport origin.

Track the sequence number or message ID of every received event. Gaps in sequence numbers indicate missed messages during a connection interruption. On reconnect, the adapter should request a replay of events since the last known good sequence number, if the broker supports it, or poll for full state to reconstruct what was missed.

Test asynchronous event handling with time-based scenarios: simulate an order submission followed by a 500ms fill event, then verify that the internal position state reflects the fill exactly once. Simulate a duplicate fill event (the same execution ID arriving twice due to a reconnect) and verify idempotency, the position should not double-count.

Worked Scenario

  1. Define the interface. You create an IBrokerAdapter interface with methods for order submission, cancellation, status queries, positions, account state, and capability discovery. You also define CanonicalOrder, OrderAck, Fill, and Position data types.
  2. Write a mock adapter. You implement MockBrokerAdapter that fills all orders immediately at the limit price. Strategy logic runs correctly against the mock, validating the interface is sufficient.
  3. Write the Alpaca adapter. AlpacaAdapter implements IBrokerAdapter. The submitOrder method translates orderType: "limit" to Alpaca's "limit" field, sends the POST to /v2/orders, and maps the response to OrderAck. The adapter also opens a WebSocket to wss://stream.data.alpaca.markets/v2/iex for trade updates.
  4. Add middleware. You wrap AlpacaAdapter with RateLimitedAdapter (200 requests per minute cap) and RetryingAdapter (exponential backoff for 5xx, no retry for 4xx).
  5. Run contract tests. You write tests that submit a canonical limit order through the Alpaca adapter against the paper trading sandbox and assert the returned OrderAck contains a valid order ID, the status is "pending_new", and the symbol matches. A second test cancels the order and asserts the status becomes "canceled".
  6. Go live. The strategy never changed. You switch from MockBrokerAdapter to the production-wrapped AlpacaAdapter by changing one configuration line. A future migration to Interactive Brokers will require writing an IBKR adapter but zero strategy changes.

Measurement Framework

MeasurementQuestion to Answer
Order submission latency (p50, p95, p99)How long does the adapter take to receive an acknowledgment from the broker?
Fill event latencyHow long after the order ACK does the fill event arrive via WebSocket vs. REST polling?
Translation error rateWhat fraction of canonical orders fail adapter-level validation before they reach the wire?
Retry rate by error codeWhich error codes are triggering retries most often, and are those retries succeeding?
Circuit breaker open frequencyHow often does the adapter's circuit breaker trip, and what is the mean time to recovery?
Duplicate event rateHow often does the adapter receive the same fill or status event more than once (indicating reconnect or replay)?

Common Failure Modes

Leaking Broker-Specific Types into Strategy Code

The most common failure is strategy code that directly imports broker SDK types: AlpacaOrder, IBKRExecution, or vendor-specific enum constants. This happens gradually, a developer needs a quick fix and reaches directly into the adapter's output. Within months, the strategy has a dozen references to broker-specific fields.

Businessman in office analyzing stock market trading data on a laptop screen.
Photo by Tima Miroshnichenko via Pexels

Prevent this by making the canonical types the only types importable from the strategy layer. The adapter module exports only normalized types; vendor types are internal implementation details. Code review should reject any import of a broker SDK type from outside the adapter module.

Authentication Expiry Without Automatic Renewal

Many brokers use short-lived OAuth access tokens (15-60 minutes) alongside long-lived refresh tokens. A common failure is submitting an order after the access token expires because the adapter never implemented automatic renewal. The order returns 401, the strategy logs an error and stops, and you discover the failure when checking P&L hours later.

Adapters must proactively refresh tokens before they expire. If a token has a 60-minute lifetime, schedule a refresh at 50 minutes. Keep the refresh as a separate background task with its own retry logic so a transient renewal failure doesn't immediately block order flow.

Silent Field Truncation or Coercion

Some brokers accept fields your canonical model doesn't explicitly map and silently ignore or coerce unrecognized values. You submit timeInForce: "gtd" (good-till-date with a specific expiry) to a broker that treats any unknown TIF as "day". The order gets submitted successfully. The broker returns 200, but expires at end of day instead of at your specified date. You don't discover this until the position is unexpectedly flat the next morning.

Prevent this by explicitly mapping every TIF and order type value in the adapter and throwing a descriptive error for any canonical value that has no broker-specific mapping. Never pass through unmapped values hoping the broker will handle them correctly.

Position State Divergence After Network Interruption

If your system loses WebSocket connectivity while a fill is in-flight, the fill event may never arrive. The adapter reconnects, restores subscriptions, and continues, but internal position state is now stale. The next strategy evaluation sees a position size that no longer matches reality, potentially triggering a spurious trade.

After every reconnection, the adapter should trigger a full state reconciliation: query all open positions from REST, compare against the internal state, and emit synthetic fill events for any discrepancies. This reconciliation should complete before the adapter signals that it is reconnected and ready to trade.

Missing Idempotency on Order Submission

Without idempotency keys, a retry after a timeout can result in two orders being submitted and both being filled. A strategy intending to buy 100 shares ends up buying 200 because the first submission succeeded, the network returned a timeout before the ACK arrived, and the retry submitted a second order.

Always generate a client-order-ID before submission, store it, and pass it to the broker if the broker supports client order IDs (most do). On a retry, reuse the same client-order-ID. The broker will recognize the duplicate and reject the second submission rather than creating a new order. If the broker doesn't support client order IDs, the query-before-retry pattern, check whether an order with the expected parameters is already open or recently filled before submitting again, is the fallback.

FAQ

Should I build one adapter per broker or one shared adapter with configuration?

One adapter class per broker. A shared configurable adapter sounds appealing but quickly becomes unmaintainable because broker differences are structural, not parametric, a different authentication flow, different WebSocket protocol, different event schemas. Configuration files can handle small differences (base URLs, rate limits) but not architectural ones. Start with one class per broker; extract a shared base class only for logic that is genuinely identical across all your adapters.

Where should I implement rate limiting, in the adapter or in a separate layer?

In a separate middleware wrapper, not inside the adapter class itself. This keeps the adapter focused on translation and lets you configure or replace rate limiting independently. Use a token-bucket or sliding-window counter per endpoint, not a single global counter, because brokers typically apply limits per route rather than globally. The middleware should expose the current limit state so your monitoring can alert before you hit the limit, not after.

How do I handle a broker that returns order fills in a non-standard format?

Write a dedicated parser method in the adapter for that broker's fill format. The method takes the raw response (JSON, FIX message, or proprietary binary) and returns your canonical Fill type. Document any fields the broker provides that your canonical model doesn't capture, you may need them later for reconciliation or fee calculations. Keep the raw response in an _raw field of the fill object for debugging, but never let downstream consumers depend on it.

Can I use a third-party broker aggregator instead of writing my own adapters?

Aggregators like Composer's own infrastructure, or commercial solutions, provide normalized access to multiple brokers through a single API. This is a valid approach if the aggregator supports all your required brokers and order types. The tradeoff is latency (an extra network hop), dependency risk (the aggregator becomes a single point of failure), and reduced control over retry and failover behavior. If you use an aggregator, you still need to understand the patterns in this hub, you're just implementing them at the aggregator interface level rather than per-broker.

How should the adapter handle a broker that rejects an order with an ambiguous error?

Ambiguous errors, where the message could mean "your order was not accepted" or "your order was accepted but we couldn't confirm it", should trigger a query-before-retry flow. Don't retry the submission immediately. Instead, query the broker for orders matching your client-order-ID or the expected parameters. If found, extract the current status and proceed accordingly. If not found after one or two queries with a short delay, retry the submission once with the same client-order-ID. Log the full resolution path for post-trade audit.

What's the minimal viable adapter for a new broker integration?

A minimal viable adapter implements: order submission (limit and market), order cancellation, order status query, and position query. These four operations, plus authentication, cover the basic order lifecycle. WebSocket event streaming, account state, buying power queries, and capability manifests can be added incrementally as your use cases require them. Start with REST polling for fills and status updates; upgrade to WebSocket streaming only after the REST flow is stable and tested.

How do I test the adapter without a real broker account?

Most major brokers provide paper trading or sandbox environments with separate API keys. Alpaca's paper trading environment, Interactive Brokers' paper account, and most exchange testnet environments accept real API calls and return realistic (if simulated) responses. For unit tests, use recorded HTTP interactions: capture real sandbox request/response pairs using a test recording library, then replay those recordings in CI. This gives deterministic tests without requiring live API access in the CI environment.

Should the adapter expose raw broker responses alongside normalized ones?

Expose them in the debug interface but never in the main interface. Include a _raw field in normalized objects that contains the original broker response for debugging and audit purposes. Downstream consumers should never branch on _raw fields, if you find yourself doing that, the canonical model is missing a field. Use _raw only for logging, support investigation, and reconciliation audits where you need to compare broker records against your records.

Where should symbol translation live in the adapter?

At the adapter boundary, in both directions, so that canonical identifiers are the only thing the rest of the system handles. Brokers use different conventions for share classes, options contracts, futures roots, and crypto pair naming, and pushing those differences inward means every downstream component has to know which broker produced a record. Keeping a per-broker mapping inside the adapter also gives one place to record unmapped symbols, which is where an integration usually first reveals gaps in coverage.

References

Educational Disclaimer

This guide is for educational and informational purposes only. It does not constitute financial, investment, or legal advice. Automated trading carries significant risk including the potential loss of your entire investment. Broker APIs, rate limits, and terms of service change frequently, always verify current documentation with your broker before building production integrations.