Skip to content

Event-Driven TypeScript: An Interview on Nimbus

Golo: Daniel, you're the CTO of Overlap in Vienna, Austria, and the lead architect behind Nimbus. Overlap isn't a framework shop, you build and ship real products for real customers, which makes the decision to build a framework all the more interesting. Before we get into the design, take us back to the beginning: what were you running into, again and again, in actual projects that made you decide to build Nimbus rather than reach for something that already existed?

Daniel: Where do I start here? It wasn't one big moment that led to the decision to build our own framework. It grew gradually, and a few things added up. One thing that always felt like a paradox to me was the fact that you need to invest a lot of time into learning most of the complex frameworks out there, and even after learning it, you often have to spend time "fighting" it. And every new developer who joins the project has to do the same. So the time spent using the framework instead of just focusing on the business logic, the clients pay us for, adds up.

Another event that showed us we need to think things differently was the AI hype of the last years. Apart from all the hype, it was a catalyst for most companies realizing the data they store within all the applications mostly shows a snapshot of the current state but do not deliver enough context and quality to fuel AI systems for proper reasoning and analytics. This led us to Event Sourcing and CQRS to build a better data foundation. The second part AI shows us is that the comfort of writing code gets less important. AI will write it for you anyway. On the other hand, the comfort of reading and understanding the code becomes even more important.

So we claimed that the framework we would like to use needs to have a focus on Event Sourcing and CQRS. It has to emphasize explicit code without a lot of framework magic in the background so you can simply read the code top to bottom like a book and instantly reason about it. And finally, you should be able to learn the concept of the framework and how to use it within less than a day.

After testing multiple frameworks in different projects we found the tool we wished for did not exist in the shape we wanted, so we built it. Not to ship a framework as a product, but because we needed it for our own work.

Golo: The JavaScript and TypeScript world has seen several attempts to bring CQRS and Event Sourcing to life, including our own wolkenkit nearly a decade ago. You've clearly studied that history. What did those earlier efforts get right, what did they get wrong, and what did you consciously decide to do differently with Nimbus?

Daniel: First, real credit where it's due. I think wolkenkit was ahead of its time. It brought CQRS and Event Sourcing to JavaScript when almost nobody in this ecosystem was talking about them, and it showed that these patterns are not just some enterprise Java thing. I have to be honest, I knew of wolkenkit, but never studied it in real depth. So I would rather not pretend to judge its internals. But the direction it pointed in was clearly right. The core idea with events as the source of truth, a clear split between writing and reading, and modeling the business properly has aged really well. It is more relevant today than ever.

What was harder back then was the foundation underneath. Frameworks had to build everything themselves, including the event store, on top of databases that were never really made for it. That is a huge amount of work, and it pulls the framework into territory that is challenging to get right. They also tended to grow big because they had to solve every part of the problem at once. TypeScript was not where it is now. Tools like Zod for schema validation did not exist. Standards like CloudEvents and OpenTelemetry were not around to build on. So those frameworks had to invent a lot that we can now just take off the shelf.

So what did we do differently? Three things, mainly. We stand on a real event store, EventSourcingDB. We keep the framework small, explicit and unopinionated instead of trying to solve everything for the user. And we lean hard on modern standards, Zod, CloudEvents, OpenTelemetry, so Nimbus can stay focused on the patterns themselves and let proven tools handle the rest.

Explicit Over Magic

Golo: Your tagline is almost a provocation: "No framework magic, just explicit code." In an ecosystem that loves decorators, dependency injection containers, and runtime metaprogramming, that's a bold stance. Where have you personally seen "magic" go wrong, and why did you make explicitness a non-negotiable from day one?

Daniel: Have you experienced a moment when everything works, you change one small thing, and suddenly something breaks in an entirely different part of the app, with no obvious connection between the two? You set a breakpoint, you step through, and you end up deep inside the framework's internals, in code you never wrote, trying to understand what it decided to do for you.

Let's take dependency injection for example. It is lovely in the demo. But in a real codebase that has grown over years, figuring out what actually gets injected, in which scope, and why, can cost you a lot of time. You get the wrong instance, or the wrong lifetime, and the answer is never in the file you are looking at. It is spread across config, decorators, and a container that resolves everything at runtime. The magic is helpful right up until the moment you need to know exactly what is happening, and then it starts to work against you.

So explicitness was non-negotiable because of what it gives you back: you read the code top to bottom, like a book, and what you see is what happens. There is no second, invisible layer of behavior. If you want to know where a value comes from, you follow it with your eyes. You do not reverse-engineer a container.

Golo: Nimbus organizes everything around three building blocks: commands, events, and queries. That's essentially the CQRS triad. How dogmatic are you about CQRS? Do you push teams into it, or do you let it emerge naturally? And for readers who still think CQRS sounds intimidating, we've argued before that it's simpler than its reputation suggests, would you agree?

Daniel: Not dogmatic at all. And I think that is important. The three building blocks, commands, events, and queries, give you a shared vocabulary, but they do not force you into full CQRS with separate read and write models from day one. You can start with all three handled in a very simple way and only split things apart when you actually feel the need.

So we do not push teams into it, we let it emerge. In practice, just naming things properly already gets you most of the value. The moment you say "this is a command, it changes something" and "this is a query, it only reads," you have already separated the two responsibilities in your head, even if they still live close together in the code. Then, when a part of the system grows, when the reads and writes really start to have different needs, you can pull them apart. But you do it because the problem asked for it, not because a framework told you to.

And yes, I completely agree it is simpler than its reputation. CQRS sounds scary because the name is four big words and people imagine event buses, separate databases, and a lot of infrastructure. But at its core it is almost boringly simple: separate the code that changes things from the code that reads things. That is it. Everything else, separate models, separate storage, projections, is optional and only comes in when you need it.

Standing on a Real Event Store

Golo: A lot of frameworks bolt Event Sourcing onto a general-purpose database and inherit all the replay and consistency headaches that come with it. You built Nimbus around EventSourcingDB from the start. What does that foundation actually buy you in practice, things like subjects or dynamic consistency boundaries?

Daniel: The short version is: we did not have to build the hard part ourselves, and we did not inherit the pile of problems that come from forcing a general-purpose database to act like an event store.

When you bolt Event Sourcing onto a normal SQL or document database, you quickly end up writing your own little event store on the side. How do you guarantee two writes do not clobber each other? How do you replay efficiently? How do you keep the order right? None of that is your business logic, it is plumbing. And it is hard to get right. EventSourcingDB was built for exactly this, so that whole category of problems is solved.

The dynamic consistency boundary is actually the biggest win for me. EventSourcingDB lets you attach preconditions when you write, things like "only accept this write if nothing else has happened to this subject since the revision I last saw." That is optimistic concurrency, and the key part is that the store enforces it atomically, at the exact moment of the write. So if two requests race to change the same thing, one of them fails cleanly instead of silently corrupting the history. In a lot of frameworks that boundary is faked by an in-memory aggregate, and an in-memory object simply cannot guarantee that the way the store can. And because the boundary is defined per write, it is flexible, you decide how strict to be for each operation, instead of being locked into one rigid rule.

Subjects are great as well. Every event lives under a subject, it is like an address, e.g. /users/abc. So "give me everything that ever happened to this user" is a natural, fast operation, not something you have to engineer. It also gives you a clean, hierarchical way to organize the log that maps directly onto your domain.

And there is a nice bonus on top: EventSourcingDB doubles as an event bus as well. When you write an event, the observers that care about it just get called. Less infrastructure, fewer moving parts, less to operate.

Golo: One design decision that stood out to us is that you treat Zod schemas as the single source of truth for every command, event, and query, with the TypeScript type derived directly from the schema. Why did that matter so much to you, and what class of bugs does it eliminate before they can ever reach a handler?

Daniel: It comes down to the idea that we want the business logic completely isolated from infrastructure concerns, and we want full type safety for it.

When you build a web API, or an RPC layer, or whatever transport, most libraries let you define validation somewhere in that layer. The problem here is that you then often end up with the shape of your data described in several places, a validation schema here, a TypeScript type there, maybe a third definition deeper in the code. And the moment you have two sources of truth, they drift. Someone updates the schema but not the type, and now the two disagree. That gap is exactly where bugs live.

So in Nimbus the Zod schema is the single source of truth, and the TypeScript type is derived directly from it. You write the schema once, and the type comes out of it automatically. They literally cannot disagree, because there is only one definition. Change the schema and the type changes with it.

And it does not matter which I/O source the data came from, an HTTP request, a message, a CLI, etc. Anything gets validated against that schema at the boundary, before it ever reaches the core. So the class of bugs this kills is the whole family of "the data was not the shape I assumed." The handler never has to second-guess its input. By the time the core sees a command, a query, or an event, it is already guaranteed to match both the runtime schema and the compile-time type. There is no defensive checking inside the business logic, and no surprising runtime errors from malformed data sneaking through.

A Functional Core, and Knowing What to Leave Out

Golo: Your write side deliberately separates a pure, deterministic core from an imperative shell that handles I/O. That's a strong, almost functional-programming choice for a TypeScript framework. Why did you draw the line there, and how does it change the testing experience for the teams using Nimbus?

Daniel: Yes, it is a functional-programming choice, but deliberately a simple one. And that is where we drew the line. I earlier mentioned a few OOP examples I am not a fan of. And the same way I don't like the heavy OOP patterns and ceremony, I also don't like the far end of functional programming, where you almost need a math degree to fully understand what is going on. So we drew the line at the basics, the easy-to-understand principles of FP: pure functions, data in and data out, no side effects, no hidden state.

You get the part of FP that makes code predictable, without the part that scares people away. That is the whole balance we were after: explicit and simple, not clever.

When it comes to testing, which goes right back to one thing that frustrated us in the first place, the separation of pure core and imperative shell is what makes testing easy. When your business logic is tangled up with I/O, every test turns into a setup project. You mock the database, fake the HTTP layer, wire up interfaces and test doubles just to check one simple rule. Most of your test code ends up being scaffolding, and barely any of it actually tests the logic you care about. With a pure core, all of that disappears. A test becomes: here is the state, here is the command, I call the function, and I check the events that come back. That is it. And for that reason, the tests start to read like a specification of the domain. You can almost read them out loud: "given a user with a pending invitation, when they accept, then an invitation-accepted event is produced." Each test is one business rule, stated plainly.

Those tests are tiny, fast, and stable. No mocks to maintain, nothing flaky to chase. That changes how teams feel about testing: when tests are this cheap and pleasant to write, people actually write them, and the most valuable part of the app ends up being the best-tested part. The messy integration stuff still gets covered, but with a smaller set of end-to-end tests that just prove the wiring holds together.

Golo: One of the things we admire about Nimbus is its restraint, what isn't there is as deliberate as what is. What did you consciously decide not to build, and how did you decide where to draw the line between giving developers helpful guidance and getting in their way?

Daniel: The clearest example is the Aggregate. If you are into Domain-Driven Design, you would expect a framework to hand you an Aggregate class, a thing that loads its own state, runs methods on itself, and emits events. We deliberately did not build that. Instead, the same idea is just a composition of small pieces: a state type, a reducer, the pure core functions, and the event store's preconditions doing the actual consistency check. Each piece is tiny and testable on its own. If a team really wants a single Aggregate class wrapping all of that, they can write one on top of our primitives in a few lines. We just did not want to force it on everyone.

Beyond that, we tried to be opinionated in only a few, deliberate places. We have a strong foundation in the Nimbus core package and the idea around commands, events, and queries. We locked in schema validation with Zod and observability with OpenTelemetry. But that is about as opinionated as it gets. The rest should be up to the developers who use Nimbus. They are in charge of their architectural and tech-stack decisions, not us.

Our philosophy is more about providing proven defaults than enforcing them. EventSourcingDB and Hono are good examples, we reach for them ourselves and we recommend them, but we do not lock you into them. For other areas we have a strong opinion on, we found it is much better to write good documentation. This way we can show the ideas and principles we recommend. And a recommendation in the docs guides you without getting in your way. The same thing baked into the framework would quietly take the decision away from you.

Ready for the Real World

Golo: Nimbus runs on both Node.js and Deno and publishes to both npm and JSR. That's extra work. Why did runtime neutrality feel important enough to pay that cost, rather than simply picking one and moving on?

Daniel: This is basically the same answer as to the question before, we do not want to force anyone into a specific tech choice, and that also includes the runtime. I also think it is important to have a thriving JavaScript and TypeScript ecosystem. A bit of competition is healthy, each time a new runtime emerged in the past, other runtimes could learn and improve from it.

When it comes to the effort it takes to achieve this, the team behind @deno/dnt (Deno to Node Transform) did a fantastic job and made it easy to build npm packages. Sticking to JavaScript standards ensures cross-runtime compatibility for the most part.

Golo: OpenTelemetry is built into Nimbus from the start, rather than something you bolt on later. That's an unusual thing to prioritize so early in a framework's life. Why did production observability earn a first-class seat at the table?

Daniel: We kind of learned this the hard way. We mostly do projects for clients, and most of them (not all) do not like to pay for non-functional things like observability. I don't know why it is so difficult to convince them, it often feels like telling a child: "the oven is hot, do not touch it" – guess what happens. Once we saw Deno had integrated OpenTelemetry it was obvious, we will do that for Nimbus as well. So by building observability in so deeply, it is just there without much extra effort.

Two Teams, Shared Assumptions

Golo: While you were building Nimbus there were a lot of conversations between our teams about consistency boundaries, subjects, and ergonomics. From your side, what was it like building a framework so tightly on top of someone else's database, and where did that collaboration actually sharpen the design? We've written before about how great minds should not think alike, they should think together, and this felt like a case in point.

Daniel: Honestly, it instantly felt like a great match. Using a database designed for exactly this one purpose was the enabler for keeping Nimbus itself so simple. The consistency boundaries, the replay, the ordering, live in the database, where they belong. Nimbus can build on top of this strong foundation.

Building so tightly on someone else's database could have been a risk, but it worked because we did not work in isolation. The direct communication between our teams helped a lot, and we share the same assumptions about how this kind of system should work. Using EventSourcingDB gave us grounded feedback, and your thinking shaped how we use subjects and lean on preconditions for the consistency boundary.

So your line about great minds thinking together rather than alike fits well. We came at the same problem from two sides, the database and the framework, and the design got better where those two views met.

Where Nimbus Goes From Here

Golo: For a TypeScript team that's curious about Event Sourcing but unsure about the tooling, where should they begin with Nimbus? Is there a kind of project or bounded context you'd steer them toward first?

Daniel: I would definitely recommend our in-depth example guide. It explains all the concepts and how to work with Nimbus in a real-world example. For everyone interested in giving Nimbus a try, I would challenge you to work through the example and put up the bet that you can learn it all the way within less than a day. Otherwise, please reach out and let me know so we can improve to lower the learning curve further.

Golo: Reaching this first milestone is really a beginning, not an end. What's on the roadmap, and what are you most excited to build, or to see the community build, next?

Daniel: Yeah, we will see where it goes from here. One thing we definitely cannot get around would be AI. I am currently playing a bit with skills for agents and how to best make it work with Nimbus. So this might be a thing to see soon. As far as I remember there is also an open issue advertising for a SQL package.

We are open for feedback and like to know what the community thinks about Nimbus.

Golo: Daniel, thank you so much for taking the time to walk us through the thinking behind Nimbus. I'm genuinely excited to see where it goes from here.

If you'd like to explore Nimbus for yourself, its home is at nimbus.overlap.at, with the source code on GitHub. And if you're weighing Event Sourcing for your next TypeScript project, or simply curious about how all of this fits together, we'd love to hear from you at hello@thenativeweb.io.

Daniel Gördes is the CTO of Overlap GmbH & Co KG, a digital agency from Vienna, Austria. For more information or to get in touch, visit the Overlap website.