OAuth flows that crash at 2 AM. Payment processors that silently accept invalid state transitions. User onboarding sequences that let people skip steps they haven't completed yet.
These bugs share a root cause: state machines that can't tell you they're broken until runtime. A new library called Tramli fixes this by moving the failure point to compile time. If your state machine has an unreachable state or a processor that requires data another processor hasn't produced yet, your code won't build.
That's not a constraint. That's a superpower.
Compile-Time Data Flow Verification
Traditional state machines let you define states and transitions, but they don't verify the data flow between them. You can write a transition from StateA to StateB that requires three pieces of data, but nothing checks whether StateA actually provides those three things.
The bug surfaces later - usually in production, usually when a user hits an edge case your tests didn't cover.
Tramli's approach is different. Every state processor declares what it requires and what it produces. The library analyses the entire graph at compile time and verifies that every processor's inputs are satisfied by some upstream processor's outputs. If they're not, the build fails.
This catches a category of bugs that unit tests miss - the ones where individual components work fine in isolation but the connections between them are wrong.
What This Looks Like in Practice
Take an OAuth login flow. You have states for: unauthenticated, authorisation code received, token exchanged, user profile fetched, session created. Each transition requires specific data - the auth code, the token, the user ID.
In a traditional state machine, you write handlers for each state and hope you got the data dependencies right. With Tramli, you declare them explicitly. The "exchange token" processor requires an auth code. The "fetch profile" processor requires a token. The "create session" processor requires a user ID.
Tramli builds a dependency graph and verifies it's valid. If you try to create a session without fetching the profile first, the compiler catches it. If you define a state that no transition can reach, the compiler catches it. If you have a circular dependency between processors, the compiler catches it.
The result: entire classes of state management bugs become compile-time errors instead of runtime crashes.
Available in Java, TypeScript, Rust
The library supports three languages, each with native type system integration. The Java implementation uses generics to enforce processor contracts. The TypeScript version use conditional types for compile-time verification. The Rust implementation uses the trait system to guarantee correctness.
That cross-language support matters because state machines show up everywhere - backend workflows, frontend UI flows, embedded systems, infrastructure orchestration. Having the same correctness guarantees across the stack means you can build complex distributed systems with stronger confidence in state consistency.
The Trade-Off
Compile-time verification isn't free. You're trading runtime flexibility for build-time safety. If you need truly dynamic state machines - ones where states and transitions are determined by user configuration or external data - Tramli won't help. The whole point is baking the structure into the type system, which means committing to it at compile time.
But most production state machines aren't that dynamic. The flow is known. The states are fixed. The data dependencies are predictable. For those cases - which is most cases - the trade-off is worth it.
Why This Pattern Matters
The broader insight here is about leveraging type systems for correctness. Modern compilers can verify a lot more than "this variable is a string." They can verify protocol compliance, state reachability, data flow consistency - all before the program runs.
The catch is you have to encode the constraints in a way the compiler understands. That's what Tramli does for state machines. It translates runtime requirements into compile-time types, then lets the type checker do the verification work.
For builders working on systems where state bugs are expensive - payment processors, authentication flows, multi-step workflows - this is worth exploring. The upfront cost is learning the library and restructuring your state logic to fit its model. The payoff is catching entire bug categories before they reach production.
The GitHub repo includes working examples for all three languages, migration guides for existing state machines, and performance benchmarks showing the compile-time verification adds negligible overhead. If you've ever debugged a state machine bug at 2 AM, you know exactly why this matters.