DEV Community

Cover image for LuciferCore: The Redemption Arc — From a Naive Prototype to a Battle-Tested .NET Framework

LuciferCore: The Redemption Arc — From a Naive Prototype to a Battle-Tested .NET Framework

Thuangf45 on July 03, 2026

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 ag...
Collapse
 
thuangf45 profile image
Thuangf45

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?

Collapse
 
thuangf45 profile image
Thuangf45

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.

Collapse
 
thuangf45 profile image
Thuangf45

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.

Collapse
 
johndoedev profile image
Johndoe

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.

Collapse
 
alexshev profile image
Alex Shev

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.

Collapse
 
xm_dev_2026 profile image
Xiao Man

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?

Collapse
 
thuangf45 profile image
Thuangf45

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.

Collapse
 
xm_dev_2026 profile image
Xiao Man

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.

Thread Thread
 
thuangf45 profile image
Thuangf45

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 OnError hook 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.

Collapse
 
nicholasthegreat profile image
Nicholas • Edited

This was so touching. I felt like being touched reading this. Beautiful.

Collapse
 
thuangf45 profile image
Thuangf45

😎

Collapse
 
lan_nguyn_47e34db57c8a55 profile image
Lan Nguyễn

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.

Collapse
 
gxnuts profile image
Hoàng Ngọc Tùng

cái thằng cha mày thuận ơi

Collapse
 
thuangf45 profile image
Thuangf45

Thank you so much!

Collapse
 
xm_dev_2026 profile image
Xiao Man

@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!

Collapse
 
buffermodeler profile image
BufferModeler

Honest question — how does the Buffer Model handle backpressure when the pool runs out under sustained load? Would love to see a benchmark at higher concurrency too.