Skip to main content

Advanced Circuit Breaker Patterns

After reviewing the basic try/catch and fallback mechanisms, use these variants as adaptation sketches. Their runtime behavior has not been assessed by a current machine gate.

Pattern 1: Circuit Breaker with Caching​

For read-heavy operations, caching is the most effective way to reduce load on downstream systems and improve resilience. If the primary data source is unavailable, the pipeline can fall back to serving slightly stale data from the cache.

Use Case: Enriching events with user metadata that changes infrequently.

Configuration​

This pipeline tries to fetch data from a cache first. On a cache miss it attempts the database call, then writes the result to the configured cache after a successful call.

cached-db-breaker.yaml
config:
# Define a cache resource
cache_resources:
- label: user_profile_cache
memory:
default_ttl: 5m # Cache entries for 5 minutes

pipeline:
processors:
# 1. Check the cache first
- branch:
processors:
- branch:
processors:
- cache:
resource: user_profile_cache
operator: exists
key: user_${!this.user_id}
result_map: root.profile_exists = content().string().bool()
- switch:
- check: this.profile_exists
processors:
- try:
- cache:
resource: user_profile_cache
operator: get
key: user_${!this.user_id}
- mapping: |
root.profile = this
root.found = true
- catch:
- mapping: |
root = {"found": false}
- processors:
- mapping: |
root = {"found": false}
result_map: |
root.profile_cached = this.found
root.user_profile = if this.found { this.profile } else { deleted() }

# 2. Act based on cache hit or miss
- switch:
# CACHE HIT: Use the cached data
- check: this.profile_cached
processors:
- mapping: |
root = this
root.data_source = "cache"

# CACHE MISS: Query the database
- processors:
- try:
- branch:
processors:
- sql_select:
driver: postgres
dsn: ${DB_CONNECTION_STRING}
table: users
columns: [user_name, user_tier]
where: user_id = $1
args_mapping: root = [ this.user_id ]
result_map: |
root.user_name = this.index(0).user_name
root.user_tier = this.index(0).user_tier

- mapping: |
root = this
root.user_profile = { "name": this.user_name, "tier": this.user_tier }
root.data_source = "database"

# On success, write the result to the cache
- cache:
resource: user_profile_cache
operator: set
key: user_${!this.user_id}
value: ${!this.user_profile.format_json()}
- catch:
# If the DB fails, mark it and continue without enrichment
- mapping: |
root = this
root.data_source = "none"
root.db_error = true

Pattern 2: Read-Write Separation​

Write operations (e.g., INSERT, UPDATE) are often more critical and have different failure modes than read operations (SELECT). It's a good practice to use separate, stricter circuit breaker policies for writes.

Use Case: A pipeline that both reads user preferences and writes new user events to a database.

Configuration​

This example uses a switch to route events based on an operation field. Writes get a very conservative circuit breaker, while reads are more tolerant.

read-write-breakers.yaml
pipeline:
processors:
- switch:
# READ operations have a tolerant circuit breaker
- check: this.operation == "read"
processors:
- try:
- sql_select:
driver: postgres
dsn: ${DB_CONNECTION_STRING}
table: users
columns: [user_name, user_tier]
where: user_id = $1
args_mapping: root = [this.user_id]
- catch:
- mapping: |
root = this
root.read_failed = true

# WRITE operations have a strict circuit breaker
- check: this.operation == "write"
processors:
- try:
- sql_insert:
driver: postgres
dsn: ${DB_CONNECTION_STRING}
table: events
columns: [event_id, payload]
args_mapping: root = [this.event_id, this.format_json()]
- catch:
- mapping: |
root = this
root.write_failed = true

output:
switch:
cases:
# Failed writes are sent to a retry queue
- check: this.write_failed == true
output:
kafka:
addresses: ['${KAFKA_BROKERS}']
topic: 'database_writes_retry'

# All other events go to a standard output
- output:
stdout: {}

Pattern 3: Adaptive Circuit Breaker​

An adaptive circuit breaker can dynamically change its behavior based on the health of a downstream system. This is an advanced technique that uses a lightweight health check to decide which circuit breaker policy to apply.

Use Case: Before calling a critical, expensive API, first ping a lightweight /health endpoint on that same API.

Configuration​

adaptive-breaker.yaml
pipeline:
processors:
# 1. Perform a quick health check
- try:
- branch:
processors:
- http:
url: ${MOCK_API_URL}/health
verb: GET
timeout: 1s
result_map: root.api_is_healthy = true
- catch:
- mapping: |
root = this
root.api_is_healthy = false

# 2. Choose a breaker policy based on health
- switch:
# If healthy, use normal settings
- check: this.api_is_healthy == true
processors:
- http:
url: ${MOCK_API_URL}/metadata/${!this.sensor_id}
timeout: 10s
retries: 5

# If unhealthy, use aggressive settings to fail fast
- processors:
- http:
url: ${MOCK_API_URL}/metadata/${!this.sensor_id}
timeout: 2s
retries: 1

These advanced patterns add explicit fallback and recovery controls. Validate their resilience and performance in the target environment.