Every software engineer has that one project.
The project that keeps you up at 3 AM.
The project where you look at your code from a few months ago and physically cringe.
For me, that project is LuciferCore.
This is the third time I am sharing this framework on Dev.to. But this time, everything is different. I am not here just to show you another update. I am here to share a story of mistakes, learning, and engineering maturity.
The Journey: Three Versions, Three Lessons
November 2025: The First Step
I was filled with pride. I launched my very first open-source package.
- In reality, the architecture was shaky.
- The code was rough and chaotic.
- It was a naive prototype, but it was a start.
March 2026: The Slap in the Face
I came back for the second time. The architecture finally had a clear shape, but it was far from ready.
- The code quality was poor.
- There were no unit tests.
- There were no benchmarks or performance tests.
- The documentation was missing, and the internal logic was unstable under real heavy load.
- I practically had to slap my younger self in the face for being careless.
Today: The Serious Return
Today, I am back with a completely different mindset. I spent the last few months doing intense self-DDoS tests, fixing core logic, and polishing the system.
LuciferCore is no longer a toy. It is a stable, mature, and serious event-driven framework built for high-performance Modulith architectures on .NET.
What Makes LuciferCore Different Now?
We focused heavily on Performance, Architecture, and Developer Experience (DX).
- The Buffer Model: Built around a Data-Oriented Buffer Model. It reduces memory allocations and optimizes CPU cache patterns.
- Minimal API Surface: The philosophy is simple: Decorate ➜ Implement ➜ Run.
-
True Modulith Design: Your components are just plain DLLs. The engine automatically discovers them using clean attributes like
[Server],[Manager],[Middleware],[Handler], and[Route].
Cold, Hard Data: The Benchmarks
We do not guess performance anymore. We verified it.
We ran a 60-second local stress test using Bombardier with 600 concurrent connections.
Most benchmarks use clean, isolated Linux servers. We did the exact opposite. We chose a brutal, messy, real-world setup:
- The Machine: A standard 8-core Intel i5 laptop.
- The OS: Windows (notorious for higher networking overhead).
-
The Stress: Both the server and the heavy load generator (
Bombardier) ran on this single laptop at the same time. - The Chaos: Normal daily background Windows applications were kept running during the test.
This means LuciferCore had to fight for every single hardware resource. Yet, the results were extreme:
- Static Files: ~160,000 requests per second (req/s).
- Pipeline Overhead: Only ~1% to 3% total framework overhead from the raw network layer all the way to the final response handler.
- Resource Stability: CPU ran at ~99% (fully saturating the remaining hardware power). Memory stayed stable at 100–300 MB.
- GC Pressure: Garbage Collection pressure was virtually zero (Gen0: 1321, Gen1: 34, Gen2: 0).
Even when choked by a standard OS and running side-by-side with its own attacker, the framework refused to drop under pressure.
Production Ready & Commercial Growth
I want this project to be highly professional. That is why I fixed the two biggest mistakes from my past:
1. Excellent Documentation
You do not need to guess how the code works. We now have a clear, step-by-step guide. If you follow the official documentation, you can get a high-performance system running in 1 to 5 minutes.
- Read the Guides: LuciferCore Official GitBook Docs
2. Dual Licensing
LuciferCore is now ready for business. It uses the AGPL-3.0 license for open-source projects. If you are building a commercial or closed-source system, a Commercial License is now available for purchase to support the project.
Connect and Support
This framework is a living ecosystem. I want to make it even better, and I cannot do it alone.
I sincerely look forward to your honest feedback, code reviews, and suggestions. Pull the package, follow the 5-minute guide in the docs, and test its limits.
- NuGet Package: Get LuciferCore on NuGet
- Official Docs: Read the GitBook Docs
- Author Profile: Nguyen Minh Thuan on Github
If this framework helps your workflow or adds value to your business, please consider supporting the project by purchasing a commercial license.
Let's build a faster, cleaner .NET ecosystem together.
Top comments (16)
Writing the "March 2026" section was the hardest part — admitting there were no tests, no benchmarks, no docs back then. That honesty is why this version feels different. Anyone else have a project like that?
Note: the benchmark ran with the load generator and server on the same laptop, on Windows, with background apps open — worst case, not best case. Happy to compare numbers if anyone reproduces it.
Modulith here means: monolith simplicity now, clean module boundaries so you can split into microservices later without a rewrite. Ask if you want details on the DLL-based module discovery.
Solid numbers. Gen2: 0 across a 60s run under real load is the part that stands out to me — most frameworks can't say that on a shared laptop with background noise. Curious how it holds up at higher concurrency.
The redemption arc is usually where the real engineering starts. A prototype proves the idea; the battle-tested version proves the boring parts: failure behavior, performance cliffs, API boundaries, and whether future contributors can reason about the system.
love the honesty about the naive prototype phase. i think most open source frameworks go through something similar — the embarrassing v1 that you secretly wish would just disappear. the modulith approach sounds interesting, how does it handle cross-module state compared to a full monolith?
Thanks for the kind words — great question!
Modules never see each other. No direct references, no awareness of one another — the core is the only thing that discovers and links everything.
Pipeline calls are fire-and-forget and stateless, for performance. A module finishes its job and responds immediately.
Writing to the shared channel is optional — it only happens when data genuinely needs to be shared. E.g. one module caches DB data so other modules in the same process can reuse it, instead of hitting the DB again.
Cross-process works the same way, just through an API layer instead of a local channel. Once the request re-enters a process, it behaves like a normal in-process call again.
So modules stay fully decoupled — state only flows through the shared channel (or API) when there's an actual reason to share it.
The "modules can't see each other" principle is clean. I've seen too many codebases where "decoupled" really just means "we added an interface layer but everything still knows about everything."
Fire-and-forget pipelines make sense for perf, but how do you handle the cases where you actually need to know if it succeeded? Like if a pipeline writes to the shared channel but silently fails — do you have any dead letter pattern, or is the assumption that the channel itself is the source of truth?
Also curious: when you say cross-process uses the same logic via API layer — are you using something like gRPC, or just REST? The overhead matters a lot when you're trying to keep the fire-and-forget semantics across process boundaries.
I completely agree. That's exactly how I feel about many codebases too. They replace direct calls with interface calls, but modules still know about each other, so the coupling is only hidden, not eliminated.
In LuciferCore, modules are discovered via reflection, compiled into delegates using expression trees, and cached. Reflection only happens during startup; after that, invocation is close to a direct call. If you're interested in the implementation details, they're documented in the Handler and Middleware sections:
bufmod.gitbook.io/lucifercore
Regarding fire-and-forget, I see it the same way as networking. We already rely on asynchronous, fire-and-forget communication every day. The transport either delivers the message or reports an error.
For cross-process communication, LuciferCore exposes an
OnErrorhook at the session layer. If the transport fails, you're notified immediately. If no transport error is raised, I trust the transport to have delivered the message.Within a single process, data is only written to the shared channel when it actually needs to be shared. For data that has been published there, the shared channel becomes the source of truth. If a consumer expects that data and it's missing, the producer either failed or never completed. The consumer can recover by fetching the data itself or asking the core to invoke the producer again.
For higher reliability, the client is responsible for awaiting responses, applying timeouts, and retrying when necessary. That retry layer complements the transport instead of replacing it.
Cross-process communication is transport-agnostic. You can use TCP, UDP, Unix Domain Sockets, or any other transport.
The overall design is heavily inspired by how operating systems and networking handle asynchronous work: components don't wait on each other unnecessarily, and coordination happens through events and well-defined communication channels. My goal isn't to force a request through a fixed chain, but to let data flow naturally while keeping modules completely unaware of each other.
This was so touching. I felt like being touched reading this. Beautiful.
😎
Respect for actually admitting v2 wasn't ready instead of just hyping it up. That's rare. Docs + benchmarks make a big difference in trust — will give it a try this weekend.
cái thằng cha mày thuận ơi
Thank you so much!
@thuangf45 that expression tree compilation approach is smart — reflection at startup, then cached delegates means you get the flexibility of dynamic discovery without the runtime overhead. Basically the best of both worlds.The OS/networking analogy makes total sense. Treating the transport layer as fire-and-forget with an OnError hook is basically how TCP works — you trust the protocol to deliver or tell you it failed. And making the transport agnostic (TCP/UDP/UDS) means the same module logic works whether you're in-process or distributed.I'll check out the GitBook docs for the Handler/Middleware implementation details. This is the kind of architecture thinking I wish more framework authors did upfront. Thanks for the detailed breakdown!