A working catalogue of general, reusable software architecture patterns, each with a concrete, illustrative example. The "In NomNomzBot" tag just shows one place each pattern happens to show up in practice — the pattern itself is technology- and project-agnostic.
What it is: Organizes a system into concentric layers (Domain, Application, Infrastructure, Presentation) where dependencies point inward only, so business rules never depend on frameworks, databases, or UI.
Example:
// Domain — pure, no external references
public sealed class Command
{
public string Name { get; init; }
public string Response { get; init; }
}
// Application — depends on Domain + an abstraction it owns
public interface ICommandRepository
{
Task<Command?> FindAsync(string name);
}
// Infrastructure — implements the abstraction with EF Core (outer layer)
public sealed class CommandRepository(AppDbContext db) : ICommandRepository
{
public Task<Command?> FindAsync(string name) => db.Commands.FirstOrDefaultAsync(c => c.Name == name);
}In NomNomzBot: the four backend projects — Domain → Application → Infrastructure → Api.
What it is: Isolates the application core behind "ports" (interfaces it defines) and plugs the outside world in through "adapters" that implement those ports, so the same core runs against swappable I/O without changing.
Example:
// Port — defined by the core, in its own terms
public interface IChatTransport
{
Task SendAsync(string channel, string message);
}
// Adapter A — Helix HTTP API (SaaS)
public sealed class HelixChatTransport(IHelixClient helix) : IChatTransport
{
public Task SendAsync(string c, string m) => helix.SendChatMessageAsync(c, m);
}
// Adapter B — IRC socket (self-host); core picks neither, DI does
public sealed class IrcChatTransport(IrcConnection irc) : IChatTransport
{
public Task SendAsync(string c, string m) => irc.PrivMsgAsync(c, m);
}In NomNomzBot: IChatTransport with Helix (SaaS) and IRC (self-host) adapters.
What it is: Wraps an incompatible interface in a new one the caller expects, letting two otherwise-mismatched components collaborate without either being rewritten.
Example:
// Third-party SDK speaks one shape; our app expects ITtsProvider
public interface ITtsProvider
{
Task<byte[]> SynthesizeAsync(string text, string voice);
}
public sealed class ElevenLabsTtsAdapter(ElevenLabsClient sdk) : ITtsProvider
{
public async Task<byte[]> SynthesizeAsync(string text, string voice)
{
ElevenLabsResponse r = await sdk.TextToSpeech(new TtsRequest { Text = text, VoiceId = voice });
return r.AudioStream.ToArray(); // translate SDK shape → our contract
}
}In NomNomzBot: ElevenLabs / Azure TTS SDKs wrapped behind a common ITtsProvider.
What it is: Defines a family of interchangeable algorithms behind one interface and lets the caller select behavior at runtime instead of branching through conditionals.
Example:
public interface IQueueStrategy<T>
{
T Next(IReadOnlyList<T> pending);
}
public sealed class FifoQueue<T> : IQueueStrategy<T>
{
public T Next(IReadOnlyList<T> p) => p[0];
}
public sealed class FairQueue<T> : IQueueStrategy<T> where T : IRanked
{
public T Next(IReadOnlyList<T> p) => p.OrderByDescending(x => x.Rank).First();
}
// Caller swaps strategy without knowing the algorithm:
public T PickNext(IQueueStrategy<T> strategy, IReadOnlyList<T> pending)
{
T next = strategy.Next(pending);
return next;
}In NomNomzBot: song-request ordering — Bamo's rank-based FairQueue<T> vs. plain FIFO.
What it is: Encapsulates a request as an object carrying everything needed to perform it, so requests can be parameterized, queued, logged, or undone independently of the invoker.
Example:
public interface ICommandAction
{
string Type { get; }
Task ExecuteAsync(PipelineContext ctx);
}
public sealed class TimeoutAction : ICommandAction
{
public string Type => "timeout";
public Task ExecuteAsync(PipelineContext ctx) =>
_mod.TimeoutAsync(ctx.TargetUserId, TimeSpan.FromSeconds(600));
}In NomNomzBot: pipeline actions (SendMessage, Timeout, Ban) as ICommandAction objects.
What it is: Passes a request along a sequence of handlers, each of which may process it and/or pass it on, decoupling the sender from the specific handler and letting steps be reordered or extended freely.
Example:
public async Task RunPipelineAsync(Pipeline pipeline, ChatMessage message)
{
PipelineContext ctx = new(message);
foreach (ICommandAction action in pipeline.Actions)
{
if (!action.Conditions.All(c => c.IsMet(ctx))) continue;
await action.ExecuteAsync(ctx);
if (ctx.Stopped) break; // a step can halt the chain
}
}In NomNomzBot: PipelineEngine running a command's ordered action chain.
What it is: Provides a single simplified entry point over a set of finer-grained subsystems, hiding their coordination so callers depend on one coherent interface instead of many.
Example:
// One call hides EventSub topics, IRC join, and command seeding underneath
public sealed class ChannelOnboardingFacade(
ITwitchEventSubService eventSub, ITwitchIrcService irc, ICommandSeeder seeder)
{
public async Task JoinAsync(string channelId)
{
await eventSub.SubscribeDefaultsAsync(channelId);
await irc.JoinAsync(channelId);
await seeder.SeedDefaultCommandsAsync(channelId);
}
}In NomNomzBot: channel join/onboarding coordinating EventSub, IRC, and command seeding.
What it is: A component declares the abstractions it needs and receives concrete implementations from an external container, inverting control of construction so the component never news-up its own collaborators.
Example:
// Depends on the interface, never constructs the concrete service
public sealed class CommandsController(ICommandService commands) : ControllerBase
{
[HttpGet]
public Task<IActionResult> List() => commands.ListAsync();
}
// Container wires the implementation once, at startup
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ICommandService, CommandService>();
// ASP.NET resolves CommandService and injects it into the controller's ctor.
}In NomNomzBot: typed services (IAuthService, ITwitchApiService) constructor-injected throughout.
What it is: Discovers types by reflecting over an assembly and matching a convention (a marker interface, base type, or name suffix), then registers them automatically instead of listing each one by hand.
Example:
// Find every ICommandAction in the assembly and register it as itself
public void RegisterCommandActions(IServiceCollection services)
{
IEnumerable<Type> actions = typeof(ICommandAction).Assembly.GetTypes()
.Where(t => typeof(ICommandAction).IsAssignableFrom(t) && t is { IsAbstract: false, IsInterface: false });
foreach (Type action in actions)
services.AddTransient(typeof(ICommandAction), action);
// Add a new action class → it self-registers, no DI edit needed.
}In NomNomzBot: pipeline actions (ICommandAction) discovered and registered by scanning Infrastructure.
What it is: Abstracts data access behind a collection-like interface so domain and application code can query and persist entities without knowing the underlying store or query API.
Example:
public interface ICommandRepository
{
Task<Command?> GetByNameAsync(string name, CancellationToken ct);
Task AddAsync(Command command, CancellationToken ct);
}
// Caller stays storage-agnostic:
public async Task<Command?> LoadShoutoutAsync(CancellationToken ct)
{
Command? cmd = await _commands.GetByNameAsync("!so", ct);
return cmd;
}In NomNomzBot: every entity is reached through a repository (e.g. ICommandRepository), never a raw DbContext in services.
What it is: Groups several data operations so they all commit or all roll back as one atomic transaction, tracking changes and flushing them with a single save.
Example:
public async Task PurchaseAsync(Guid userId, Guid itemId, CancellationToken ct)
{
using IUnitOfWork uow = _unitOfWork;
CatalogItem item = await _catalog.GetAsync(itemId, ct); // the item defines its own cost
await _wallet.DebitAsync(userId, item.Cost, ct); // write 1
await _inventory.GrantItemAsync(userId, itemId, ct); // write 2
await uow.SaveChangesAsync(ct); // both commit, or neither
}In NomNomzBot: economy purchases (debit points + grant item) wrap both writes in one IUnitOfWork.
What it is: Models expected failure as a value returned from the function rather than a thrown exception, forcing the caller to handle success and error paths explicitly.
Example:
public Result<Command> Find(string name)
{
Command? cmd = _commands.GetByName(name);
return cmd is null
? Result<Command>.Fail("Command not found")
: Result<Command>.Ok(cmd);
}
public IActionResult Lookup()
{
Result<Command> result = Find("!so");
if (!result.Success) return Problem(result.Error);
return Ok(result.Value);
}In NomNomzBot: service operations return Result<T> for expected failures instead of throwing.
What it is: CQRS stands for Command Query Responsibility Segregation. It splits what is normally one model into two: a command side (operations that change state — create / update / delete) and a query side (operations that read state). Each side gets its own model and code path, optimized and scaled for its job, instead of forcing a single shared model to serve both reads and writes.
Example:
// Write side — validates and mutates
public Task Handle(CreateTimer cmd) =>
_timers.AddAsync(new Timer(cmd.Name, cmd.IntervalSeconds));
// Read side — returns a denormalized view, no domain logic
public Task<TimerListView> Handle(GetTimers query) =>
_readDb.QueryTimerListAsync(query.ChannelId);In NomNomzBot: dashboard read views (stats aggregation) are queried separately from the command/timer write paths.
What it is: Encapsulates a query predicate as a reusable, composable object, so business rules for "which entities match" live in one named place instead of being scattered across ad-hoc queries.
Example:
public sealed class ActiveTimersSpec : Specification<Timer>
{
public ActiveTimersSpec(Guid channelId) =>
Criteria = t => t.ChannelId == channelId
&& t.IsEnabled
&& !t.IsDeleted;
}
public async Task<IReadOnlyList<Timer>> LoadDueAsync(Guid channelId)
{
IReadOnlyList<Timer> due = await _timers.ListAsync(new ActiveTimersSpec(channelId));
return due;
}In NomNomzBot: selecting enabled, non-deleted timers due to fire for a channel.
What it is: Marks a record as deleted with a flag instead of physically removing it, preserving history and referential integrity while hiding it from normal queries via a global filter.
Example:
public void Delete(Command cmd) => cmd.IsDeleted = true;
// Global query filter hides soft-deleted rows automatically
modelBuilder.Entity<Command>()
.HasQueryFilter(c => !c.IsDeleted);In NomNomzBot: all entities carry IsDeleted + an EF Core global query filter; nothing is ever physically DELETEd.
What it is: Routes write traffic to a primary database and read traffic to one or more replicas, scaling read throughput while the primary handles consistency for writes.
Example:
public DbConnection ForRead() => new NpgsqlConnection(_replicaConnStr);
public DbConnection ForWrite() => new NpgsqlConnection(_primaryConnStr);
// Heavy dashboard read hits a replica; writes go to primary
public async Task<IEnumerable<ViewerRow>> LoadViewersAsync(string sql)
{
await using DbConnection conn = _factory.ForRead();
IEnumerable<ViewerRow> rows = await conn.QueryAsync<ViewerRow>(sql);
return rows;
}In NomNomzBot: high-volume read endpoints (community/viewer lists, dashboard stats) read from a replica while writes target the primary.
What it is: Persists state as an append-only sequence of events rather than the current snapshot; the current state is rebuilt by replaying those events, giving a full audit history.
Example:
public int RebuildBalance(Guid userId)
{
IPointsEvent[] events = new IPointsEvent[]
{
new PointsEarned(userId, 50),
new PointsSpent(userId, 20),
new PointsEarned(userId, 10),
};
int balance = events.Aggregate(0, (bal, e) => e.ApplyTo(bal)); // → 40
return balance;
}In NomNomzBot: append-only points/loyalty ledger, balance derived by replay.
What it is: Builds a denormalized, query-optimized view from a stream of events or writes, separating how data is read from how it is stored so reads stay fast and shaped for the UI.
Example:
// Update a flat read model as events arrive — reads never touch the event log
void On(PointsEarned e) => _leaderboard[e.UserId] += e.Amount;
void On(PointsSpent e) => _leaderboard[e.UserId] -= e.Amount;
IReadOnlyList<Row> TopTen() =>
_leaderboard.OrderByDescending(kv => kv.Value).Take(10).Select(Row.From).ToList();In NomNomzBot: dashboard leaderboard/stats widgets fed from the ledger.
What it is: Evolves a schema or contract without downtime in three phases — expand (add the new shape alongside the old), migrate (dual-write/backfill so both work), then contract (remove the old shape once nothing uses it).
Example:
-- Expand: add new nullable column, write to both
ALTER TABLE viewers ADD COLUMN display_name text; -- old: username still read
-- Migrate: backfill + dual-write in app code
UPDATE viewers SET display_name = username WHERE display_name IS NULL;
-- Contract: once all readers use display_name
ALTER TABLE viewers DROP COLUMN username;In NomNomzBot: renaming/reshaping a column on a live table via staged EF Core migrations so deploys never break in-flight reads.
What it is: A style where components communicate by producing and reacting to events rather than calling each other directly, decoupling producers from consumers so the system can scale and evolve independently.
Example:
// Producer raises an event; it knows nothing about who handles it
await _eventBus.PublishAsync(new StreamWentOnlineEvent(channelId, startedAt));
// Independent reactors each handle it on their own
class PostGoLiveTweet : IEventHandler<StreamWentOnlineEvent> { /* ... */ }
class StartUptimeTimer : IEventHandler<StreamWentOnlineEvent> { /* ... */ }In NomNomzBot: Twitch EventSub notifications fanned out to feature handlers.
What it is: A messaging pattern where publishers send messages to a named channel/topic and any number of subscribers receive them, with the broker handling delivery so neither side knows the other.
Example:
// Subscribers register interest in a topic
bus.Subscribe("chat.message", h => RenderInDashboard(h));
bus.Subscribe("chat.message", h => RunCommandParser(h));
// One publish reaches every current subscriber
await bus.PublishAsync("chat.message", new ChatMessage("!sr Daft Punk"));In NomNomzBot: SignalR hub broadcasts (dashboard/overlay) and Redis pub/sub fan-out.
What it is: A first-class object recording that something meaningful happened in the domain, raised by an aggregate so side effects can be triggered without the aggregate depending on them.
Example:
public sealed record ViewerSubscribed(Guid ChannelId, string UserId, int Months) : IDomainEvent;
public void ApplySubscription(string userId, int months)
{
Tier = SubTier.Tier1;
_domainEvents.Add(new ViewerSubscribed(Id, userId, months)); // recorded, dispatched after save
}In NomNomzBot: entities raising events (e.g. ViewerSubscribed) that fire pipeline responses.
What it is: Writes outgoing messages to a database table in the same transaction as the state change, then a separate relay publishes them — guaranteeing the message is sent if and only if the state was committed.
Example:
public async Task RedeemRewardAsync(Reward reward)
{
using IDbContextTransaction tx = await _db.BeginTransactionAsync();
_db.Rewards.Add(reward);
_db.Outbox.Add(new OutboxMessage("reward.redeemed", Serialize(reward))); // same tx
await _db.SaveChangesAsync();
await tx.CommitAsync();
// A background relay later reads unsent Outbox rows and publishes them.
}In NomNomzBot: reward-redemption events queued for reliable EventSub/overlay delivery.
What it is: Lets a consumer safely process the same message more than once by recording handled message IDs and skipping duplicates, so at-least-once delivery does not cause duplicate side effects.
Example:
public async Task HandleAsync(EventSubNotification msg)
{
if (!await _seen.TryAddAsync(msg.MessageId, ttl: TimeSpan.FromMinutes(10)))
return; // already processed — drop the redelivery
await _dispatcher.DispatchAsync(msg.Event);
}In NomNomzBot: EventSub message-ID dedup so retried Twitch notifications fire effects once.
What it is: A long-lived component started at application boot that runs work continuously or on a schedule in the background, independent of any request. It owns its own lifecycle (start/stop) and runs off the request thread.
Example:
public sealed class TimerDispatcher : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await _timers.FireDueMessagesAsync(ct);
await Task.Delay(TimeSpan.FromSeconds(30), ct);
}
}
}In NomNomzBot: TwitchEventSubService and TwitchIrcService running as IHostedService.
What it is: Decouples work submission from work execution through a shared queue — producers enqueue items without blocking, and one or more consumers drain the queue at their own pace. Smooths bursts and bounds concurrency.
Example:
public async Task PumpAsync(ChatMessage message, CancellationToken ct)
{
Channel<ChatMessage> channel = Channel.CreateBounded<ChatMessage>(1000);
// Producer (EventSub handler)
await channel.Writer.WriteAsync(message, ct);
// Consumer (background loop)
await foreach (ChatMessage msg in channel.Reader.ReadAllAsync(ct))
await _pipeline.ExecuteAsync(msg, ct);
}In NomNomzBot: incoming chat messages queued from EventSub, drained by the pipeline engine.
What it is: Permits bursts up to a bucket capacity while enforcing a steady long-run rate — tokens refill at a fixed rate, each action spends one, and actions with no token available are delayed or rejected. Smooths traffic against a downstream limit.
Example:
public bool TryConsume()
{
double now = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
_tokens = Math.Min(_capacity, _tokens + (now - _last) * _refillPerSec);
_last = now;
if (_tokens < 1) return false;
_tokens -= 1;
return true;
}In NomNomzBot: throttling outbound chat sends against Twitch's per-channel message limits.
What it is: Partitions a shared resource into isolated compartments so that exhaustion or failure in one compartment cannot starve or sink the others. Each lane gets its own bounded capacity.
Example:
public async Task AcquireLaneAsync(Lane lane, CancellationToken ct)
{
// Separate concurrency budgets per lane — a flood of low-priority
// work can never consume the slots reserved for commands.
Dictionary<Lane, SemaphoreSlim> lanes = new()
{
[Lane.Command] = new SemaphoreSlim(8), // mod/command actions
[Lane.Chat] = new SemaphoreSlim(4), // normal chat pipelines
[Lane.Overlay] = new SemaphoreSlim(2), // alert widgets
};
await lanes[lane].WaitAsync(ct);
}In NomNomzBot: separate execution lanes for command/event pipelines vs. bulk chat processing.
What it is: When intake outpaces capacity, the system pushes back (blocks/slows producers) or deliberately drops lower-value work rather than growing an unbounded queue until it collapses. Bounded buffers make overload explicit instead of fatal.
Example:
public void Enqueue(Alert alert)
{
// Bounded queue that drops the newest item under sustained overload
Channel<Alert> channel = Channel.CreateBounded<Alert>(new BoundedChannelOptions(500)
{
FullMode = BoundedChannelFullMode.DropWrite
});
if (!channel.Writer.TryWrite(alert))
_metrics.Increment("overlay.alerts.shed");
}In NomNomzBot: overlay alert bursts shed when the widget queue is saturated.
What it is: Wraps calls to a failing dependency and, after a failure threshold, "opens" to fail fast for a cooldown instead of hammering it — then probes with a half-open trial before closing again. Protects both caller and the struggling dependency.
Example:
public async Task<HelixResponse> SendGuardedAsync(HelixRequest req, CancellationToken ct)
{
if (_state == State.Open && DateTime.UtcNow < _openUntil)
throw new CircuitOpenException("Twitch Helix unavailable");
try
{
HelixResponse result = await _helix.SendAsync(req, ct);
_failures = 0;
_state = State.Closed;
return result;
}
catch
{
if (++_failures >= 5)
{
_state = State.Open;
_openUntil = DateTime.UtcNow.AddSeconds(30);
}
throw;
}
}In NomNomzBot: guarding Helix/Spotify API calls so an outage fails fast instead of cascading.
What it is: Retries a transient failure with progressively longer waits (often doubling), plus jitter to avoid synchronized retry storms. Recovers from blips without overwhelming the recovering dependency.
Example:
public async Task<HelixResponse> SendWithRetryAsync(HelixRequest req, CancellationToken ct)
{
for (int attempt = 0; ; attempt++)
{
try { return await SendAsync(req, ct); }
catch (HttpRequestException) when (attempt < 5)
{
TimeSpan delay = TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 100);
TimeSpan jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 100));
await Task.Delay(delay + jitter, ct);
}
}
}In NomNomzBot: EventSub WebSocket reconnect backoff after a dropped connection.
What it is: When multiple instances run for availability, they coordinate so exactly one is elected "leader" to perform singleton work, while the rest stand by to take over if the leader dies. Prevents duplicate execution of work that must happen once.
Example:
public async Task<bool> TryAcquireLeadershipAsync()
{
// Whoever sets the lock key first becomes leader; others retry.
bool isLeader = await _redis.StringSetAsync(
"leader:eventsub", _instanceId,
expiry: TimeSpan.FromSeconds(15),
when: When.NotExists);
if (isLeader)
await _redis.KeyExpireAsync("leader:eventsub", TimeSpan.FromSeconds(15)); // renew lease
return isLeader;
}In NomNomzBot: ensuring one instance owns each channel's EventSub WebSocket across a scaled-out deployment.
What it is: Orders pending work by fairness rather than pure arrival time, interleaving across submitters or ranks so no single heavy user monopolizes the queue and no item is starved indefinitely. Balances throughput with equitable access.
Example:
// Round-robin across requesters so one viewer can't flood the queue.
fun nextTrack(perUser: Map<String, ArrayDeque<Track>>): Track? {
for ((user, tracks) in perUser) { // cursor rotates each call
val track = tracks.removeFirstOrNull() ?: continue
return track.also { lastServed = user }
}
return null
}In NomNomzBot: the song-request queue using Bamo's rank-based FairQueue<T> instead of FIFO.
What it is: The application checks a cache first; on a miss it loads from the source of truth, populates the cache with a TTL, and returns the value. Reads stay fast and the cache only holds data that's actually requested.
Example:
public async Task<ChannelInfo> GetChannelAsync(string id, CancellationToken ct)
{
ChannelInfo? cached = await _cache.GetAsync<ChannelInfo>($"channel:{id}");
if (cached is not null) return cached;
ChannelInfo fresh = await _helix.GetChannelInfoAsync(id, ct);
await _cache.SetAsync($"channel:{id}", fresh, TimeSpan.FromMinutes(5));
return fresh;
}In NomNomzBot: caching Twitch channel/stream info in Redis to avoid repeated Helix calls.
What it is: A mechanism by which services announce themselves and clients locate them on a network by name and capability, without hardcoded addresses or a central registry — typically by multicasting queries and listening for advertised records.
Example:
# Service advertises itself on the local link via mDNS
_nomnomz-overlay._tcp.local. PTR desk-pc._nomnomz-overlay._tcp.local.
desk-pc._nomnomz-overlay._tcp.local. SRV 0 0 5080 desk-pc.local.
desk-pc._nomnomz-overlay._tcp.local. TXT "version=1" "path=/hubs/overlay"
# Client multicasts a query, gets back host:port — no IP was configured
$ dns-sd -B _nomnomz-overlay._tcp # browse for the service
Browsing for _nomnomz-overlay._tcp
ADD desk-pc _nomnomz-overlay._tcp.local.
In NomNomzBot: OBS browser-source / overlay clients discovering the self-hosted API on the local network.
What it is: Instead of a client polling for changes, the producer pushes an HTTP request to a consumer-registered URL the moment an event occurs, inverting control so the consumer is notified rather than asking.
Example:
// Consumer exposes a callback URL; producer POSTs an event to it
[HttpPost("webhooks/twitch/eventsub")]
public async Task<IActionResult> Receive([FromBody] EventNotification e)
{
if (!Verify(Request.Headers["Twitch-Eventsub-Message-Signature"], rawBody))
return Unauthorized();
await _events.PublishAsync(e.Subscription.Type, e.Event); // react, don't poll
return Ok();
}In NomNomzBot: integration callbacks (e.g. Discord/YouTube push notifications) hitting registered endpoints.
What it is: A single logical event stream is partitioned across many transport shards so load distributes horizontally, letting one subscription scale to volumes no single connection could carry while consumers read from any shard.
Example:
public async Task SetUpConduitAsync(ITwitchClient twitch, string broadcasterId)
{
// One logical "conduit" fans events out across N shards (sockets/queues)
Conduit conduit = await twitch.CreateConduitAsync(shardCount: 5);
// Each shard owns a slice of the firehose; add subscriptions to the conduit, not a socket
foreach (ConduitShard shard in conduit.Shards)
await shard.ConnectAsync(); // events for any channel arrive on whichever shard owns it
await twitch.SubscribeAsync(conduit.Id, "channel.chat.message", broadcasterId);
}In NomNomzBot: Twitch EventSub Conduits fanning chat/event topics across shards on the SaaS deployment.
What it is: Peers connect straight to each other over a persistent transport with no intermediary message broker in the path, trading central routing for lower latency and one fewer moving part to operate.
Example:
public async Task ConnectAndReadAsync(CancellationToken ct)
{
// No broker — the client opens a socket directly to the source and reads the stream
ClientWebSocket ws = new();
await ws.ConnectAsync(new Uri("wss://eventsub.wss.twitch.tv/ws"), ct);
while (ws.State == WebSocketState.Open)
{
EventSubMessage msg = await ReceiveAsync(ws, ct); // events delivered peer-to-peer, no queue hop
await HandleAsync(msg);
}
}In NomNomzBot: the self-host EventSub WebSocket and IRC connections talking directly to Twitch with no broker in between.
What it is: Release strategies that swap or replace running instances without downtime — blue-green keeps two full environments and flips traffic atomically; rolling replaces instances a few at a time so the service stays up throughout.
Example:
# Rolling: replace 1 pod at a time, never below healthy capacity
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # at most one old instance down at a time
maxSurge: 1 # spin up one new instance before retiring an old one
# Blue-green: green is live, deploy to blue, then flip the router
# router.upstream = "blue" → atomic cutover; instant rollback by pointing back to "green"In NomNomzBot: zero-downtime API rollouts behind the load balancer on the SaaS deployment.
What it is: A runtime switch that gates a code path so behavior can be turned on or off without redeploying — decoupling release of code from release of functionality and enabling progressive rollout.
Example:
public async Task<IActionResult> ShowEmotePickerAsync(Channel channel)
{
// Code ships dark; the flag decides whether the path runs at runtime
if (await _flags.IsEnabledAsync("emote-picker", channel.Id))
return BttvEmotePicker(channel); // new behavior, on for a subset
return LegacyEmoteList(channel); // everyone else, unchanged
}In NomNomzBot: progressive-scope features and per-channel toggles (e.g. 18+ gambling gate) flipped without a redeploy.
What it is: An endpoint the runtime polls to learn an instance's state — liveness reports "the process is alive," readiness reports "ready to receive traffic" — so orchestrators route around or restart unhealthy instances.
Example:
// Liveness: am I running? Readiness: are my dependencies reachable?
builder.Services.AddHealthChecks()
.AddNpgSql(pgConn, name: "postgres") // not ready until DB answers
.AddRedis(redisConn, name: "redis");
app.MapHealthChecks("/health/live", new() { Predicate = _ => false }); // process up
app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Count == 0 }); // deps upIn NomNomzBot: the /health/live and /health/ready probes the orchestrator polls before sending traffic.
What it is: A delegated-authorization flow where a client redirects the user to an authorization server, receives a short-lived code, then exchanges that code server-side for access and refresh tokens — so the user's credentials and the long-lived secret never pass through the browser.
Example:
1. Client → browser → GET https://id.twitch.tv/oauth2/authorize
?response_type=code&client_id=abc&scope=chat:read&state=xyz&redirect_uri=.../callback
2. User approves → Twitch redirects: .../callback?code=AUTH_CODE&state=xyz
3. Server (back-channel) → POST https://id.twitch.tv/oauth2/token
grant_type=authorization_code&code=AUTH_CODE&client_secret=SECRET
4. ← { "access_token": "...", "refresh_token": "...", "expires_in": 14400 }
In NomNomzBot: Twitch login at /api/v1/auth/twitch/callback, routed by state (user/bot/channel_bot).
What it is: A self-contained, cryptographically signed token carrying claims about the bearer, presented on each request so the server can authenticate statelessly by verifying the signature instead of a session lookup.
Example:
public JwtSecurityToken IssueToken(Guid userId, SecurityKey key)
{
JwtSecurityToken token = new(
issuer: "nomnomzbot", audience: "nomnomzbot",
claims: new[] { new Claim("sub", userId.ToString()) },
expires: DateTime.UtcNow.AddMinutes(60),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
// client then sends it on every call:
// Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
return token;
}In NomNomzBot: issued after OAuth, sent as Authorization: Bearer; hubs receive it as ?access_token=<jwt>.
What it is: Access decisions are expressed as named policies — reusable rules evaluated against the caller's identity and context (claims, or a live permission lookup) — rather than role checks scattered through handlers, letting authorization logic be defined once and applied declaratively.
Example:
// A policy name IS a permission/action key. A custom requirement + handler resolves
// THIS user's permission for THIS tenant at request time — not a static claim match.
public sealed class PermissionRequirement(string key) : IAuthorizationRequirement
{
public string Key { get; } = key;
}
public sealed class PermissionHandler(IActionAuthorizationService authz, ITenantContext tenant)
: AuthorizationHandler<PermissionRequirement>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context, PermissionRequirement requirement)
{
Guid userId = context.User.GetUserId();
Result allowed = await authz.AuthorizeActionAsync(userId, tenant.BroadcasterId, requirement.Key);
if (allowed.IsSuccess)
context.Succeed(requirement);
}
}
// A policy provider builds the requirement from the policy name on demand,
// so open-ended action keys need no per-key registration.
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionHandler>();
[Authorize(Policy = "moderation:ban")] // policy name == Gate-2 action key
public Task<IActionResult> BanUser(/* ... */) { /* ... */ }In NomNomzBot: [Authorize(Policy = "<key>")] where the policy name is a Gate-2 action key (or a Plane-C IAM key); the handler checks that one user's one permission for the current tenant via IActionAuthorizationService / IPlatformIamService.
What it is: Permissions are assigned to named roles rather than to individuals; users acquire permissions by holding a role, so access management reduces to assigning and revoking roles.
Example:
enum ManagementRole { Broadcaster, Moderator, Vip, Subscriber, Everyone }
static bool Can(ManagementRole role, string action) => action switch
{
"command:edit" => role <= ManagementRole.Moderator, // Broadcaster or Moderator
"song:request" => role <= ManagementRole.Subscriber,
_ => false
};In NomNomzBot: the canonical ManagementRole / CommunityStanding vocabulary gating command and pipeline actions.
What it is: Authority is carried by an unforgeable token (a capability) that itself grants a specific action — possession is permission — so access is granted by handing out scoped tokens instead of checking identity against a central permission list.
Example:
public OverlayCapability IssueOverlayCapability(Guid overlayId)
{
// An overlay link IS the authority — no identity check, the token grants exactly one thing.
OverlayCapability capability = new(
OverlayId: overlayId,
Scope: "render:alerts", // this token can ONLY render alerts for this overlay
Token: Guid.CreateVersion7());
// holder presents it: GET /overlay?cap=<token> → renders, no login required
return capability;
}In NomNomzBot: OBS browser-source overlay URLs — the unguessable token in the link is the grant.
What it is: Every actor starts with no access and is granted only the minimum permissions its task requires; anything not explicitly allowed is denied by default.
Example:
bool IsAllowed(string action, IReadOnlySet<string> grantedScopes) =>
grantedScopes.Contains(action); // absent ⇒ denied, no implicit allow
// baseline: empty grant set → everything denied until a scope is opted in
public bool CheckBaseline()
{
HashSet<string> perms = new(); // default-deny
perms.Add("song:request"); // opt in one capability
return IsAllowed("moderation:ban", perms); // false
}In NomNomzBot: opt-in / default-deny baseline — out-of-box authority is only Twitch's own role rules, nothing extra.
What it is: Instead of demanding all permissions up front, the application requests each authorization scope only at the moment the user enables the feature that needs it, minimizing the consent surface and building trust incrementally.
Example:
// Enabling a feature requests its scope ONLY if we don't already hold it.
public Task<ScopeGrantState> EnableFeatureAsync(Guid channelId, string feature)
=> _scopes.EnsureFeatureScopesAsync(channelId, feature);
// required ⊆ granted → AlreadyGranted: enable now, NO re-auth round-trip
// otherwise → authorize URL for (granted ∪ required): one consent for just the delta
// Every token refresh reconciles to Twitch's authoritative scope list; a dropped
// scope disables only the features that depended on it (never a blind reconnect).
public Task<IReadOnlyList<string>> OnTokenRefreshedAsync(Guid connectionId, IReadOnlyList<string> granted)
=> _scopes.ReconcileGrantedScopesAsync(connectionId, granted);In NomNomzBot: progressive Twitch scopes via IScopeGrantService — channel:manage:raids requested only when raid responses are enabled, skipped when already granted, and a revoked scope disables just its dependent features.
What it is: Stored data is encrypted with a key held separately from the datastore, so a database dump or stolen disk yields only ciphertext that is useless without the key.
Example:
public async Task StoreTokenAsync(Guid tenantId, byte[] tokenBytes)
{
// The data key is unwrapped through the key vault — the OS-native secure store on
// self-host (DPAPI / Keychain / libsecret), a KMS on SaaS — never read from a column.
byte[] dek = await _keyVault.GetDataKeyAsync(tenantId);
byte[] cipher = _aead.Encrypt(tokenBytes, dek, associatedData: tenantId.ToByteArray()); // AES-256-GCM
await _store.SaveTokenAsync(tenantId, cipher); // only ciphertext reaches the DB
}In NomNomzBot: Twitch/Spotify OAuth tokens stored AES-256-GCM under a per-tenant DEK; the root KEK is custodied by the OS secure store on self-host (Credential Locker/DPAPI · Keychain · libsecret) or a KMS on SaaS — never a plaintext key in a column or env var.
What it is: Per-subject data is encrypted under a dedicated key; to "erase" that data irreversibly you simply destroy its key, rendering the still-stored ciphertext permanently undecryptable without touching every row.
Example:
// Data is written encrypted under the tenant's own key...
public async Task StoreSecretAsync(Guid tenantId, ViewerSecrets secrets)
{
byte[] key = await keyVault.GetKeyAsync(tenantId);
await _store.SaveAsync(tenantId, Encrypt(secrets, key));
}
// ...so right-to-erasure is a single key deletion: every ciphertext for this
// tenant becomes permanently unreadable — no row-by-row DELETE needed.
public Task EraseTenantAsync(Guid tenantId) =>
keyVault.DeleteKeyAsync(tenantId);In NomNomzBot: per-subject/tenant right-to-erasure — destroying that subject's DEK makes all their stored ciphertext permanently unreadable, with no row-by-row delete.