Case study
Asuka RS
2025 - alpha Sole developer
Summary
Asuka RS is an alpha Rust side project for an event-driven Discord bot. I built the architecture around independent event tasks, two separate command registries, cache-first lookups, localized responses, and support for 31 locale codes. It is a study in giving each concern its own boundary instead of collapsing everything into one handler.
Context
The project explores a bot architecture that can route text and interaction commands, manage user preferences, and provide configurable moderation controls without a single handler owning all of it. Rust makes the boundaries explicit: independent tasks, typed command contexts, and caches whose eviction is part of the type, not an afterthought.
Role
Sole developer responsible for architecture, the Rust implementation, and internationalization.
Runtime and event flow
A single Twilight gateway shard receives events. Each event is added to the Twilight message cache and then handed to its own Tokio task, so an error handling one event cannot stop the ones behind it. Raid-relevant events pass through the anti-raid pre-filter first; then main_handler routes by event kind into one of two dispatch paths (text commands and interactions) which keep separate registries and validation.
Runtime / gateway to dispatch
Every event runs in its own task, on two registries
Components (buttons and select menus) dispatch straight to their registered handler and deliberately skip the command validation ladder, because they were already reachable only through a message the bot itself sent.
The command protection ladder
Both command kinds climb a ladder of checks before their handler runs, but not the same one. Allowance and user permissions are checked on both paths; a configured cooldown is claimed atomically before the rate limit is even tested; and only then does the handler run. Slash commands additionally verify bot permissions and can load the database user when a tiered limit needs the user’s tier; the text path currently does neither.
Command protection / the check ladder
Text and slash commands climb different ladders
Cache-first data access
User access is cache-first through an in-memory Moka cache. On a hit, the request never touches the database; on a miss, UserService upserts the user through Prisma, caches the resulting model, and returns it. The user and server caches are each capped at 100 MiB with a one-hour time-to-idle expiry, and the user cache weighs its entries from the key and language length.
Data access / cache-first
A cache miss upserts SQLite, then repopulates the cache
Cooldowns, rate-limit counters, and anti-raid detection windows each live in their own async Moka caches, so none of them adds database pressure. That is the theme: state that is cheap to rebuild lives in memory with a bounded weight and a TTL, and only durable facts reach SQLite.
Internationalization
User-facing strings go through fluent-templates, with one .ftl file per locale under locales/. The language is resolved from Discord’s interaction locale, normalized against the 31 application locale codes; a changed preference is written to the database and the cache together. Every lookup carries a colocated fallback, and the Fluent fallback language is English, so a missing key degrades to readable text rather than an error.
Internationalization / Fluent
Discord's locale, the saved preference, then English
31 locale codes
Supported in one system
Translation coverage is not assumed complete: locale files may still carry generated TODO: translate placeholders, and a dedicated locales binary scans the source for lookup calls and synchronizes the .ftl files, adding missing keys and removing dead ones.
Anti-raid
Guild protection runs as a pre-filter on raid-relevant events. Mass joins, repeated messages, message saturation, and destructive role or channel spikes are tracked in in-memory sliding windows, so detection never queries the database on the hot path; per-guild configuration is cached and loaded on demand. When a window crosses its threshold, a configurable action ladder decides the response, from silent logging up to a full lockdown, with a development guild always treated as test mode.
Anti-raid / detection to response
Sliding windows in memory, an action ladder per guild
Tradeoffs
Independent event tasks were the right concurrency model for the MVP, and they made the next-stage need obvious: there is no API or dashboard yet, so configuration is code and data rather than a product surface. Isolating cooldown, rate-limit, and detection state into bounded in-memory caches keeps SQLite for durable facts, at the cost of that state being per-process and lost on restart.
Reliability & Security
The implementation includes multi-layer permissions, per-user cooldowns, optional and tiered rate limits, parameterized Prisma queries, localized errors, and configurable anti-raid responses. A failed command is isolated to its own task and converted to a localized Discord reply rather than crashing the bot. These are implementation controls, not production-scale guarantees.
What I would improve
I would add an API and a dashboard before a broader release, so guild configuration and moderation settings become a product surface instead of database rows.
Building an event-driven integration that needs clear boundaries and a maintainable core? Discuss your project.