Surgery on a Beating Heart
Splitting 846 files into 6 assemblies without breaking anything.
For engineers and curious players.
The god object was tamed. Events no longer went through a single choke point. But the codebase was still one massive blob — 846 files in a single project, with no boundaries between skill calculations and screen drawing, between settings and serialization, between everything.
To fix the scheduler (and actually solve the crash), I needed to isolate it. And to isolate anything in this codebase, I first needed to split the codebase apart.
That's the surgery. Splitting a running application into layers without breaking it.
The Monolith
EVEMon.Common. 846 files. 83,656 lines. One assembly. Seventy-five directories.
Everything lived together: interfaces, encryption, email, in-game browser, serialization, Windows controls, skill math, character models, settings — all compiled as a single unit.
For non-engineers: imagine a warehouse with no internal walls. Kitchen, bedroom, bathroom, and garage are all one room. Change the plumbing, risk the electrical. Replace the carpet, and you might move the refrigerator. That's what working in EVEMon.Common was like.
In practice: the skill planning logic imported Windows UI types. The market order models imported image-drawing libraries. The settings system called MessageBox.Show() directly from service code. Seven character model files had Windows-only image types baked into the domain layer.
You couldn't extract the scheduler into something testable because it was tangled into the same assembly as the Windows screen code. And you couldn't test the screen code because it was tangled into the same assembly as the settings code. Everything touched everything.
Designing the Split
I designed six assemblies forming a strict hierarchy — dependencies only flow downward, never up, never sideways:
EveLens.Core → Interfaces only. Zero dependencies. The constitution.
↓
EveLens.Data → Enums, constants, static game data.
↓
EveLens.Serialization → API response formats, settings file formats.
↓
EveLens.Models → Characters, Skills, Plans.
↓
EveLens.Infrastructure → EventAggregator, logging, scheduling.
↓
EveLens.Common → Services, settings, ViewModels.464 files extracted from Common. Each layer has one job.
The original codebase had 21 interfaces total, and zero of them were service interfaces. Zero IEventAggregator. Zero IDispatcher. Zero ISettingsProvider. The entire architecture relied on EveMonClient as the sole point of access for everything.
EveLens.Core defines 27 service contracts. Any project in the stack can depend on Core without pulling in anything else. This is what makes the scheduler testable — it depends on interfaces, not on the Windows UI.
How to Split a Running Codebase
You can't stop the world while you rearrange 846 files. The application needed to keep working after every step.
I moved files one category at a time. Interfaces first — they became EveLens.Core. Then enums and constants became EveLens.Data. Then serialization DTOs. Then models. Then infrastructure.
Each move was a separate commit. Each compiled. Each was tested. When a move created a circular dependency — file A needs file B, but file B also needs file A — I had to extract an interface from one of them and break the cycle.
The key commit that landed the split:
19ed6784 — Project Phoenix: 6-assembly split of EVEMon.Common (356 files extracted)356 files physically moved from one project to six. One commit. Everything still compiles.
The Guard Rails
During the split, things kept going wrong in predictable ways. A developer (or an AI) would instinctively put a file back in Common because that's where "everything goes." An interface would grow a Windows dependency. A service would grab a static reference instead of using injection.
I codified the patterns into fourteen rules — Architectural Laws — and wrote tests to enforce every one:
- No static mutable state — use AppServices and interfaces
- No god objects — no class over 500 lines or referenced by over 30 files
- Dependencies flow downward only — enforced by automated tests
- New services must be testable — constructor injection, interfaces, test doubles
- Events through EventAggregator only — no static events, ever
- Lazy by default — constructors must be fast
- No sync-over-async — never
.Wait()or.Resultexcept in bootstrap - Right file, right assembly — interfaces in Core, enums in Data, DTOs in Serialization
- EveMonClient is frozen — never add to it, only subtract
- All async void must have try/catch — WinForms requires async void for handlers
- Event subscriptions must be disposed — store the disposable, dispose on close
- Tests prove behavior — no feature without a test, no bug fix without a regression test
- Serialization DTOs are the data contract — changes require round-trip tests
- No direct EveMonClient access from UI — use AppServices
Each law has corresponding automated tests. Assembly boundary tests use cycle detection on the actual assembly references at runtime — if someone introduces a circular dependency, the build fails and tells them exactly which path creates the cycle.
32 architecture enforcement tests total. The architecture can't regress because the tests won't let it.
The Result
EVEMon was 2 assemblies with 846 files tangled together. EveLens is 7 assemblies with zero circular dependencies and 14 laws enforced by 32 architecture tests.
| Metric | Before | After |
|---|---|---|
| Meaningful assemblies | 2 | 7 |
| Files in Common | 846 | 507 (-339) |
| Windows imports in business logic | 66 | 0 |
| Service interfaces | 0 | 27 |
| Architecture tests | 0 | 32 |
| Circular dependency detection | None | Automated at build time |
The monolith had walls. The foundation was set. The scheduler — the thing that started this whole journey — could finally be isolated, tested, and rebuilt.