Fallback Bindings
A fallback binding is a last-resort candidate: it is registered only when no other non-fallback binding exists for the same key. If any regular binding is present, the fallback is silently skipped.
This makes fallbacks ideal for providing sensible defaults — especially in libraries, where a component can ship with a built-in implementation that application code (or other library users) can silently replace without any configuration change.
import { Fallback } from '@caffeine-projects/dicaf/decorators'
Basic usage
abstract class Logger {
abstract log(msg: string): void
}
// Ships with the library — used only when the application provides nothing else
@Fallback()
@Injectable()
@Extends()
class NoopLogger extends Logger {
log(_msg: string) {}
}
When no other Logger binding is registered, NoopLogger is the resolved instance.
When any non-fallback Logger is present — from the application or another module —
NoopLogger is skipped entirely.
The library pattern
The most common use for @Fallback is in reusable libraries. The library ships a
working default; consumers override it by declaring their own binding.
// --- library code ---
abstract class Cache {
abstract get(key: string): unknown
abstract set(key: string, value: unknown): void
}
@Fallback()
@Injectable()
@Extends()
class InMemoryCache extends Cache {
private readonly store = new Map<string, unknown>()
get(key: string) { return this.store.get(key) }
set(key: string, value: unknown) { this.store.set(key, value) }
}
// --- application code (optional override) ---
@Injectable()
@Extends()
class RedisCache extends Cache {
constructor(private readonly client: RedisClient) {}
get(key: string) { return this.client.get(key) }
set(key: string, value: unknown) { this.client.set(key, JSON.stringify(value)) }
}
When the application registers RedisCache, InMemoryCache is never instantiated.
When it does not, InMemoryCache is used automatically — no import or configuration
required.
@Fallback on @Provides methods
@Fallback works on factory methods inside a @Configuration class. This is useful
when a library ships a full configuration class with some methods acting as defaults
and others as mandatory bindings.
const kCache = Symbol('Cache')
const kMetrics = Symbol('Metrics')
// --- library config ---
@Configuration()
class LibConfig {
@Fallback()
@Provides(kCache)
cache(): Cache {
return new InMemoryCache() // overridable default
}
@Provides(kMetrics)
metrics(): Metrics {
return new NoopMetrics() // always registered — not a fallback
}
}
// --- application config (optional) ---
@Configuration()
class AppConfig {
@Provides(kCache)
cache(): Cache {
return new RedisCache(process.env.REDIS_URL!) // takes precedence
}
}
When both configs are loaded, AppConfig.cache wins and LibConfig.cache is skipped.
LibConfig.metrics is always registered because it is not a fallback.
Combining @Fallback with @ConditionalOn
Both decorators apply independently. A @Fallback + @ConditionalOn binding is
registered only when the condition passes and no other non-fallback binding exists
for the key. If either check fails, the binding is skipped.
@Fallback()
@ConditionalOn(() => process.env.NODE_ENV !== 'test')
@Injectable()
@Extends()
class DefaultPaymentGateway extends PaymentGateway {
// active in non-test environments when no other gateway is registered
}
In test environments the condition fails, so the fallback is never registered —
leaving room for a test stub to be the only gateway, or for optional(PaymentGateway)
to return undefined.
Manual bindings with .fallback()
The fluent binder exposes .fallback() for decorator-free containers.
import { DiCaf } from '@caffeine-projects/dicaf'
const di = new DiCaf({ decorators: false })
// Library registers its default
di.bind(Cache)
.toClass(InMemoryCache)
.fallback()
// Application optionally registers an override — wins over fallback
di.bind(Cache)
.toClass(RedisCache)
await di.init()
di.get(Cache) // RedisCache — InMemoryCache was skipped
If the second bind call is absent, InMemoryCache is used.
@Fallback vs @ConditionalOn
Both can suppress a binding, but the mechanism differs.
@Fallback | @ConditionalOn | |
|---|---|---|
| Trigger | Another non-fallback binding exists for the key | A predicate returns false |
| Resolution point | After all bindings are collected | During init() predicate evaluation |
Combines with @ConditionalOn | Yes — both checks apply | — |
| Best for | Optional library defaults, safe overrides | Environment flags, presence checks |
Use @Fallback when the suppression condition is "something else already handles this."
Use @ConditionalOn when the suppression condition is "a runtime fact is not met."
See the Conditional Bindings guide for full @ConditionalOn
documentation.