Skip to content

Repository files navigation

Carom 🎱

A lean, fast, and safe resilience library for .NET

NuGet License .NET

Carom is a zero-dependency resilience library that enforces best practices by default. Named after the billiards shot where the ball bounces before reaching its target, Carom helps your code gracefully handle failures.

Runs on .NET 10 and .NET 8. The core packages target netstandard2.0, so .NET Framework and older .NET are supported too; the ASP.NET Core, EF Core and OpenTelemetry packages target net8.0;net10.0. Tests run on both runtimes in CI.

🎯 Why Carom?

  • Zero Dependencies (core packages)
  • Zero Allocations (0 bytes on the successful hot path, test-enforced)
  • Safe by Default (mandatory decorrelated jitter)
  • Tiny Footprint (25.5KB core, 54KB extensions)
  • Fully Composable (all patterns work together)

πŸ“¦ Packages

Sizes are the Release-built assemblies. Tests fail if the core grows past 28KB or the extensions past 60KB.

Package Size Purpose
Carom 25.5KB Core retry + timeout
Carom.Extensions 54KB Circuit Breaker, Fallback, Bulkhead, Rate Limiting
Carom.Http 12.5KB HTTP integration
Carom.DependencyInjection 21.5KB Named resilience pipelines for IServiceCollection
Carom.AspNetCore 8.5KB ASP.NET Core health checks
Carom.EntityFramework 9.5KB EF Core retry
Carom.Telemetry.OpenTelemetry 9.5KB OpenTelemetry metrics for retries, circuit opens and rejections, after one CaromTelemetry.Subscribe() call

πŸš€ Quick Start

Installation

dotnet add package Carom
dotnet add package Carom.Extensions

Basic Usage

using Carom;

// Simple retry with exponential backoff
var result = await Carom.ShotAsync(() => api.CallAsync(), retries: 3);

// With timeout
var bounce = Bounce.Times(5).WithTimeout(TimeSpan.FromSeconds(30));
var data = await Carom.ShotAsync(() => apiClient.FetchAsync(), bounce);

Circuit Breaker

using Carom.Extensions;

var cushion = Cushion.ForService("payment-api")
    .OpenAfter(failures: 5, trackingLast: 10)
    .WithinLast(TimeSpan.FromMinutes(1))
    .When(ex => ex is HttpRequestException)
    .HalfOpenAfter(TimeSpan.FromSeconds(30));

var payment = await CaromCushionExtensions.ShotAsync(
    () => paymentApi.Charge(), 
    cushion);

The circuit opens as soon as 5 failures are recorded. trackingLast bounds how far back failures are counted, and WithinLast expires them by age, so an old incident cannot combine with a fresh failure to trip the breaker. When decides which exceptions count as the dependency's fault: without it, a bug in your own calling code would open the circuit on a healthy service. Retries run inside the breaker, so one logical call records one outcome no matter how many attempts it took.

Fallback

var config = await new Func<Task<AppConfig>>(() => configService.LoadAsync())
    .PocketAsync(AppConfig.Default);

Bulkhead

var dbCompartment = Compartment.ForResource("database")
    .WithMaxConcurrency(10)
    .Build();

var query = await CaromCompartmentExtensions.ShotAsync(
    () => db.QueryAsync(sql), 
    dbCompartment);

Rate Limiting

var apiThrottle = Throttle.ForService("external-api")
    .WithRate(100, TimeSpan.FromSeconds(1))
    .WithBurst(20)
    .Build();

var apiResult = await CaromThrottleExtensions.ShotAsync(
    () => apiClient.CallAsync(), 
    apiThrottle);

πŸŽ“ Patterns

Pattern Class Purpose
Retry Carom Exponential backoff with jitter
Timeout Bounce.WithTimeout() Operation timeout
Circuit Breaker Cushion Prevent cascade failures
Fallback Pocket/PocketAsync Graceful degradation
Bulkhead Compartment Concurrency control
Rate Limiting Throttle Token bucket algorithm

Documentation

🀝 Contributing

Contributions welcome! Please read CONTRIBUTING.md first.

πŸ“„ License

MPL-2.0 - see LICENSE for details.

πŸ™ Acknowledgments

Built with the Baryo.Dev philosophy: zero dependencies, minimal allocations, safe by default.


Made with ❀️ by Baryo.Dev

πŸ“Š Performance

Speed is not the pitch. Per successful call, both Carom and Polly cost nanoseconds, invisible next to the network or database call being wrapped. What Carom offers is small, allocation-free, dependency-free resilience for hosts where the standard stack is too much: .NET Framework and netstandard2.0 services, size-constrained deployments, and libraries that should not impose a dependency graph.

The claims we do make are measured (Apple M1, .NET 8, against Polly 8.4.2) and enforced by tests/Carom.Tests/PublishedClaimsTests.cs:

  • Zero allocations on the successful hot path: Carom 0 B per call. Polly v8 allocates 24 B per call, the Polly v7 API 248 B.
  • Small on disk: Carom.dll is 25.5 KB and Carom.Extensions.dll 54 KB. Polly.Core.dll (net8.0) is 237 KB.
  • Zero package dependencies on every target: Polly.Core has none on net8.0 but needs four packages on netstandard2.0 and five on .NET Framework.

Details and methodology in docs/BENCHMARKS.md.

About

Carom is a zero-dependency resilience library that enforces best practices by default.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages