Skip to content
Reliability InfrastructureOpen source

RelPrim

RelPrim is an open-source Python reliability layer for operations that cross process, network, or provider boundaries. It brings retries, timeouts, fallbacks, circuit breakers, validation, and execution reporting into one explicit operation model without hiding the domain decisions that make recovery safe.

Status
Active
Role
Creator & Maintainer
Category
Reliability Infrastructure
Source
Open source

Case study

External calls are easy to write and difficult to operate.

A provider integration often begins with one asynchronous function. Production requirements then accumulate around it. The call needs a timeout. Some failures should be retried. The result may need validation. A fallback may become necessary. Repeated failures should affect whether more work is admitted. When the operation ends, the team needs enough evidence to understand what happened.

RelPrim is an open-source Python library I created to give those concerns a small, explicit execution model.

It provides reusable reliability mechanics without pretending that infrastructure can decide whether a business operation is safe to repeat or whether a degraded result remains valid for the product.

Problem

Calls that cross a process, network, or provider boundary fail in more ways than their function signatures suggest.

A remote operation may:

  • exceed the time the caller is willing to wait;
  • fail temporarily and succeed on another attempt;
  • return a response that is structurally valid but unusable;
  • leave the primary provider unavailable while an alternative still works;
  • trigger repeated failures against a dependency that is already unhealthy;
  • succeed only after several attempts;
  • produce a value without preserving any record of the path that produced it.

Applications usually address these problems gradually.

A timeout is added inside one client. Retry logic appears in a decorator elsewhere. A fallback is implemented with a broad exception handler. Validation lives in application code after the provider call. Logging records the final exception but loses the intermediate decisions.

The result is not simply duplicated code. It is duplicated reliability semantics.

Two operations that look similar may retry different exceptions, use different timeout meanings, or report failures in incompatible ways. The policy is spread across control flow, helper functions, SDK configuration, and operational conventions.

That makes the system difficult to review before failure and difficult to explain after it.

Why I built RelPrim

I created RelPrim after repeatedly seeing external integrations follow the same path.

The first implementation is usually direct:

response = await provider.generate(prompt)

That is a reasonable place to start.

The problem appears when reliability requirements are added one at a time. Individual mechanisms are not especially difficult to implement. The harder part is keeping their behavior coherent as the integration grows.

A retry should know which failures are temporary. A timeout should have a clear scope. A fallback should be identifiable in the final result. Invalid responses should not be accepted as successful merely because the transport returned 200. Operational data should describe the complete execution rather than one isolated request.

Copying these mechanics into each integration creates drift.

Hiding them inside a generic client creates a different risk. The code may claim that an operation is resilient without showing what that resilience means.

RelPrim grew from the tension between those two outcomes.

I wanted the mechanics to be reusable while keeping the policy visible in the application. A reviewer should be able to see that an operation retries, times out, validates its result, uses a fallback, or participates in circuit protection. The library should provide the execution model, but the application should remain responsible for the decisions that depend on business meaning.

The design question

The central design question was not:

How can another retry decorator be implemented?

It was:

What should an external operation return when its execution path matters as much as its value?

A normal function returns a value or raises an exception.

That interface is often too narrow for a reliability layer. Two executions can produce the same value after taking very different paths.

One may succeed on the first attempt.

Another may time out, retry, reject an invalid response, switch to a fallback, and finally produce a usable result.

Both may satisfy the immediate caller. They are not operationally equivalent.

RelPrim therefore treats the business value and the execution history as two parts of the same result.

Solution

RelPrim wraps an existing asynchronous operation rather than replacing the provider client or the application workflow around it.

The conceptual model is:

Application operation
        |
        v
Named RelPrim execution
        |
        +--> timeout policy
        |
        +--> retry policy
        |
        +--> validation policy
        |
        +--> circuit breaker
        |
        +--> fallback chain
        |
        +--> structured events
        |
        v
OperationResult[T]
        |
        +--> business value
        |
        +--> execution report

The application still owns the callable and its domain semantics.

RelPrim owns the reusable mechanics around its execution and preserves a structured account of what happened.

This boundary keeps the library useful across provider SDKs, HTTP clients, workers, services, and orchestration code without turning it into another integration framework.

Two levels of adoption

RelPrim provides two public entry points built on the same underlying primitives.

The decorator API is intended for integrations where a concise policy is enough.

from relprim import resilient


@resilient(
    retries=3,
    timeout=10,
)
async def call_provider(prompt: str) -> str:
    return await provider.generate(prompt)


result = await call_provider("Write a short product summary")

The policy remains visible at the function boundary. Adoption does not require rewriting the provider integration around a new client abstraction.

For operations that need more deliberate composition, RelPrim exposes the async_operation(...) builder.

from relprim import RetryPolicy, TimeoutPolicy, async_operation


result = await (
    async_operation("generate_response", call_provider)
    .with_retry(RetryPolicy(max_attempts=3))
    .with_timeout(TimeoutPolicy(seconds=10))
    .run("Write a short product summary")
)

The builder gives the operation an explicit name and keeps its selected reliability mechanisms in one definition.

This is useful when a policy is assembled dynamically, uses several primitives, or needs structured lifecycle events and reporting.

The two APIs serve different levels of complexity without creating two separate execution models.

Named operations

The builder requires an operation name:

async_operation("generate_response", call_provider)

This looks like a small API choice, but it reflects a broader design principle.

Reliability behavior needs a stable operational identity.

Names appear in execution reports and structured events. They allow metrics, logs, tests, and diagnostics to describe a capability rather than an anonymous callable.

A name such as:

generate_response

communicates intent.

A name such as:

call

or:

invoke

describes implementation mechanics and provides little operational value.

The operation name is not intended to contain request-specific data. It should remain stable across executions so that behavior can be aggregated and compared.

Operation results

A decorated function or builder execution returns an OperationResult[T] instead of the raw business value.

result = await call_provider(prompt)

value = result.value
report = result.report

This makes execution metadata part of the contract rather than a side effect hidden in logs.

The caller can use the business value normally while retaining access to the report for diagnostics, testing, persistence, or observability.

That distinction also prevents a successful fallback from erasing the fact that the primary operation failed.

From the product perspective, the returned value may be acceptable.

From the engineering perspective, the execution path still matters. A growing fallback rate may indicate that the primary dependency is degrading long before the user-visible operation begins to fail.

The result model preserves both truths.

Execution reports

RelPrim produces structured execution reports that can be inspected or serialized.

print(result.report.to_dict())

The report provides one place to understand how the operation was executed.

Its purpose is not to replace logs, metrics, or tracing. It is to preserve execution-level facts before an observability backend is chosen.

This keeps the core model transport-agnostic.

An application may:

  • inspect reports in tests;
  • attach them to structured logs;
  • persist selected information;
  • translate them into metrics;
  • expose them through internal diagnostic tooling;
  • correlate them with higher-level workflows.

The library does not require every consumer to adopt the same telemetry stack.

Structured lifecycle events

Reports describe the completed execution. Some systems also need to observe the operation while it is running.

RelPrim can emit structured lifecycle events through an opt-in event emitter.

from relprim import EventEmitter, InMemoryEventSink


event_sink = InMemoryEventSink()
event_emitter = EventEmitter(sinks=(event_sink,))

result = await (
    async_operation("generate_response", call_provider)
    .with_events(event_emitter)
    .with_retry(RetryPolicy(max_attempts=3))
    .run(prompt)
)

The event model is independent of one logging or tracing product. Event sinks can translate lifecycle information into the observability system chosen by the application.

Events are disabled by default.

This is deliberate. A reusable library should not silently produce logs, emit telemetry, or introduce operational cost without the application choosing that behavior.

Retry policies

RelPrim supports explicit retry policies, including exponential backoff with jitter.

The important design choice is not the presence of retrying itself. It is that retry behavior can be defined independently from the operation and kept visible at the boundary.

For simple adoption, the decorator accepts a retry count:

@resilient(retries=3, timeout=10)
async def call_provider(prompt: str) -> str:
    return await provider.generate(prompt)

For more precise behavior, the caller can provide a policy and classify which exceptions are safe to retry.

class TemporaryProviderError(Exception):
    pass


@resilient(
    retries=3,
    retry_on=(TemporaryProviderError,),
    timeout=10,
)
async def call_provider(prompt: str) -> str:
    return await provider.generate(prompt)

Production integrations should prefer narrow retry classification.

Catching every exception may look resilient while repeating failures that are permanent, caused by invalid input, or unsafe to execute again.

RelPrim provides the retry mechanism. The application decides which failures have the semantics required to use it.

Timeout enforcement

Timeouts bound how long an asynchronous attempt is allowed to run.

They prevent a slow or stalled dependency from holding application resources indefinitely.

The simple API accepts a timeout directly. The advanced API uses an explicit TimeoutPolicy.

result = await (
    async_operation("generate_response", call_provider)
    .with_timeout(TimeoutPolicy(seconds=10))
    .run(prompt)
)

RelPrim treats async cancellation and timeout behavior as part of the execution model rather than an incidental wrapper around the provider SDK.

A timeout still cannot prove that a remote side effect did not occur.

That distinction belongs to the operation’s domain semantics. The library can stop waiting and report what happened locally. It cannot infer the final state of a remote system it does not control.

This is one example of a boundary RelPrim is designed not to hide.

Fallback chains

A fallback is not merely another exception handler.

It is an explicit decision that another operation can produce an acceptable result when the primary path fails.

RelPrim supports named fallback candidates:

from relprim import fallback_chain


result = await (
    async_operation("generate_response", call_primary_provider)
    .with_retry(RetryPolicy(max_attempts=2))
    .with_fallbacks(
        fallback_chain(
            ("backup_provider", call_backup_provider),
        )
    )
    .run(prompt)
)

The candidate name appears in reports and structured events.

That makes degraded execution observable. The application can distinguish a normal primary success from a result produced by a backup path.

RelPrim does not decide whether using a fallback is correct.

Switching between two read providers may be safe. Switching providers after an ambiguous side-effecting operation may duplicate work. The policy has to reflect the meaning of the operation rather than treating every provider as interchangeable.

Circuit breakers

RelPrim includes asynchronous circuit breakers for protecting the application and an unhealthy downstream dependency.

from relprim import CircuitBreaker


circuit_breaker = CircuitBreaker(
    name="primary_provider",
    failure_threshold=3,
    recovery_timeout_seconds=30,
)

result = await (
    async_operation("generate_response", call_primary_provider)
    .with_circuit_breaker(circuit_breaker)
    .with_retry(RetryPolicy(max_attempts=3))
    .run(prompt)
)

The breaker has an explicit name because its state represents an operational boundary that should be identifiable.

It does not repair failed work. It decides whether new attempts should continue reaching a dependency after repeated failure.

The surrounding application still needs to decide what happens when the circuit is open. It may use a fallback, defer durable work, return a degraded response, or fail immediately.

Result validation

Transport success is not always application success.

A provider may return an empty response, malformed content, an incomplete payload, or a value that violates the caller’s expectations while still using a successful protocol status.

RelPrim supports validation policies that run before a result is accepted as successful.

from relprim import validation_policy, validator


response_validation = validation_policy(
    validator(
        "non_empty_response",
        lambda value: bool(value.strip()),
        message="Response must not be empty.",
    )
)

result = await (
    async_operation("generate_response", call_provider)
    .with_validation(response_validation)
    .run(prompt)
)

Validation failures are captured in the execution report.

They can also participate in retry behavior when the application explicitly classifies them as retryable.

This is especially useful at boundaries where a dependency can be available but still return an unusable result.

Reliability is not only the ability to obtain a response. It is the ability to obtain a result that satisfies the operation’s contract.

Composition without hidden policy

RelPrim brings the selected mechanisms into one operation definition.

result = await (
    async_operation("generate_response", call_primary_provider)
    .with_retry(RetryPolicy(max_attempts=3))
    .with_timeout(TimeoutPolicy(seconds=10))
    .with_validation(response_validation)
    .with_circuit_breaker(circuit_breaker)
    .with_fallbacks(
        fallback_chain(
            ("backup_provider", call_backup_provider),
        )
    )
    .with_events(event_emitter)
    .run(prompt)
)

The value of this API is not that every external call should enable every primitive.

Most should not.

Its value is that the operation’s reliability behavior becomes visible in one reviewable place. A reviewer can see which mechanisms participate and ask whether they match the semantics of the operation.

The library remains intentionally small enough that this definition does not become a separate workflow language.

Important engineering decisions

Wrap integrations instead of replacing them

RelPrim accepts existing callables.

It does not require applications to abandon provider-native SDKs, introduce a new HTTP client, or move domain behavior into the library.

This keeps the integration boundary narrow and makes incremental adoption possible.

Keep the business value and execution history together

OperationResult[T] prevents a usable return value from erasing the path that produced it.

This is particularly important for retries, fallbacks, validation, and operational diagnosis.

Provide simple and explicit APIs

The decorator covers common cases without requiring builder configuration.

The builder exposes composition when the operation needs more control.

Both use the same primitives, which reduces the risk that convenience and advanced usage develop different semantics.

Make observability part of execution

Reports and lifecycle events are generated from the operation model.

Observability is not reconstructed later from a collection of unrelated log messages.

Keep events opt-in

Libraries should not create hidden telemetry behavior.

Applications choose whether events are emitted and where they are sent.

Prefer named operations and candidates

Stable names make reports and events useful across executions.

They also force the application to describe what capability is being protected.

Do not claim domain safety

A timeout, retry, circuit breaker, or fallback can be executed correctly while the overall business behavior remains wrong.

RelPrim exposes mechanics. The application owns the invariant.

What RelPrim deliberately does not replace

RelPrim is not:

  • an HTTP client;
  • an AI provider SDK;
  • a task queue;
  • a workflow engine;
  • an observability backend;
  • a replacement for provider-native clients;
  • a replacement for orchestration systems.

It is a reliability layer used inside an application, service, worker, or orchestration workflow.

Keeping this boundary narrow is an architectural decision.

A library that attempts to own transport, orchestration, persistence, observability, and business recovery eventually becomes another platform that the application must work around.

RelPrim focuses on the execution boundary around a callable.

Current scope and roadmap

The current public API includes:

  • the @resilient decorator;
  • explicit retry policies;
  • exponential backoff with jitter;
  • asynchronous timeout enforcement;
  • fallback chains;
  • asynchronous circuit breakers;
  • validation policies and callable validators;
  • structured execution reports;
  • typed operation results and execution errors;
  • lifecycle events and event emitters;
  • an asynchronous operation builder;
  • idempotency policies with concurrent execution joining and successful result replay;
  • provider-aware rate-limit recovery with bounded waiting and Retry-After support.

The roadmap includes persistent event storage, OpenTelemetry integration, additional validation adapters, and broader support for durable idempotency stores.

RelPrim should remain honest about the guarantees it provides today.

Its idempotency support can coordinate callers and replay completed results, but the application still owns the idempotency key, storage durability, retention policy, and the business invariant that makes repetition safe.

Its rate-limit handling can respect provider-supplied delays and keep waiting bounded by policy, but the application still decides whether waiting, failing, or falling back is correct for the operation.

RelPrim can already help teams make recurring execution decisions explicit without pretending to replace durable workflow state, distributed coordination, reconciliation, or domain-specific recovery.

Challenges and trade-offs

A small API versus complete flexibility

Reliability libraries can accumulate configuration quickly.

Every provider exposes different behavior. Every application has different failure semantics. Supporting every variation directly would make RelPrim as difficult to understand as the ad hoc code it is intended to replace.

The current direction favors a focused set of primitives with clear responsibilities.

Unusual domain behavior can remain in application code rather than becoming another generic option.

Convenience versus visible semantics

A decorator makes adoption easy, but excessive convenience can hide important policy decisions.

The builder exists for operations where the mechanisms need to be more visible. The simple API should remain a concise entry point rather than grow into a long list of opaque keyword arguments.

Useful reports versus coupling to observability vendors

Execution reports and events need enough structure to support diagnostics without making the core library depend on one telemetry product.

The transport-agnostic model adds some integration work for applications, but it keeps RelPrim usable across different operational environments.

Reusable mechanics versus domain correctness

The library can classify exceptions, execute retries, enforce timeouts, and record outcomes.

It cannot determine whether repeating an external side effect is safe.

Refusing to blur that boundary makes the library less magical, but more honest.

Lessons

Building RelPrim reinforced that reliability is not a collection of defensive wrappers.

The difficult part is preserving the meaning of an operation while its execution passes through retries, timeouts, validation, fallback behavior, and dependency health decisions.

The second lesson is that observability should begin inside the execution model.

Once a function returns a raw value or raises a generic exception, much of the useful context may already be gone. Keeping the value, report, operation name, candidate identity, and lifecycle events connected makes later diagnosis more reliable.

The third lesson is that reusable infrastructure should make decisions visible without pretending to make every decision itself.

RelPrim provides a vocabulary and execution layer for operations at unreliable boundaries. The application still decides what is safe, what is acceptable, and what recovery means.

That separation is not a limitation to work around.

It is the central design constraint that keeps the library useful without creating false guarantees.

  • AI Creative PlatformActive

    Ghostviber

    Founder & Engineer

    Ghostviber is an AI creative platform built around the full songwriting workflow, from the first line to generated audio and artwork. I designed and built the product architecture, multilingual rhyme engine, AI orchestration, credit ledger, asynchronous workflows, storage, and reliability mechanisms needed to turn variable external providers into one coherent user experience.

    • AI orchestration
    • Distributed workflows
    • Usage-based billing
  • Payments & IntegrationsPast Work

    Making Payment Workflows Reliable

    Payment webhooks can arrive late, more than once, or after an ambiguous timeout. This case study explores how durable ingestion, idempotent processing, and explicit recovery paths protect financial correctness at the boundary with external providers.

    • Idempotency
    • Webhooks
    • Async processing

Let's talk systems

Building a system that has to remain predictable under pressure?

I help teams shape the systems their products depend on, especially early on or when reliability becomes critical. I also write and speak about these problems. Tell me what you’re building and where it hurts.