About
I am a backend engineer in Istanbul. I build what sits underneath a product: real-time backends, multi-tenant platforms, the pipelines that feed them, and the panels a team runs them from. Three products stand on a substrate I extracted from a fourth. Games as well, client and server both.
How I work
- Write the decision down
- The code says what was built. A decision record says why, and that is the part somebody needs a year later, usually while deciding whether to undo it. The count is in the card below, counted from the repositories themselves.
- Own the whole path
- A feature is not finished at the pull request. I would rather carry it from the schema to the screen to the deploy that puts it live than hand it across three boundaries and hope.
- Build the tool
- When the same work happens by hand twice, it becomes a tool. A balancing pipeline in a game and an operator panel at work are the same instinct pointed at different problems.
- Prefer boring
- I pick the least surprising thing that solves the problem, and spend what that saves where the work is genuinely hard.
What I build with
- Games
- Unity and C# on the client, .NET and PostgreSQL on the server, with EF Core between them.
- Product
- PHP and TypeScript, Slim and NestJS, MySQL, PostgreSQL and Redis, with the WebSocket daemons beside the HTTP application. Python where the work is a pipeline rather than a request.
- Enterprise
- NestJS on Bun, Next.js, Drizzle and MikroORM over Postgres and MySQL, CASL holding the permissions, and whatever the reporting turns out to need.
Working together
The team I work in is four engineers. Three are on the mobile clients and I am mostly on the backend. When we replaced the real-time voice provider on the voice platform, the change could not stop at my side of the API, so I spent that migration committing to their repository, on their branch, rather than sending a changelog and waiting.
- On my own
- Being the only engineer on something changes how early I write it down, not how much. The documentation is the only colleague the next person gets, so it goes in while the reasoning is still fresh rather than reconstructed a quarter later.
On GitHub
- Decision records
- 1,011
An ADR is the one artefact that only exists because somebody else was going to read it. These are across 38 repositories.
Repositories started, per year
TypeScript 33%C# 31.9%JavaScript 11.4%HTML 10.3%Python 7.5%SCSS 2.5%Other 3.4%
13 public sealed record LootResult(bool Dropped, RolledItem? Item, CurrencyDrop? Currency);21 public static class LootPipeline22 {23 // Stateless components, safe to share across all ops (ADR-0020 component interpreter).24 private static readonly IReadOnlyDictionary<string, ILootComponent> Registry =25 new Dictionary<string, ILootComponent>26 {27 ["gate"] = new GateComponent(),28 ["loot-class"] = new LootClassComponent(),29 ["rarity-roll"] = new RarityRollComponent(),30 ["type-roll"] = new TypeRollComponent(),31 ["class-roll"] = new ClassRollComponent(),32 ["item-level"] = new ItemLevelComponent(),33 ["mod-fill"] = new ModFillComponent(),34 ["currency-roll"] = new CurrencyRollComponent(),35 ["bonus-rolls"] = new BonusRollsComponent(),36 };3738 // Class routing (SPEC-REWARD-SPINE §4): loot-class decides which branch's components apply. Skipping a39 // component skips its rng draws too — both toolchains run this identical routing, so parity holds.40 private static readonly HashSet<string> ItemOnly =41 new HashSet<string> { "rarity-roll", "type-roll", "class-roll", "item-level", "mod-fill" };49 private static readonly HashSet<string> Deferred =50 new HashSet<string> { "unique-roll", "pity" };54 public static LootResult Execute(ConfigPipeline pipeline, RollContext ctx, IRandom rng, KernelConfig cfg)55 {56 foreach (var stage in pipeline.Stages)57 {58 if (!Registry.TryGetValue(stage.Component, out var component))59 {60 if (Deferred.Contains(stage.Component)) continue; // valid but unimplemented in Slice 1 → no-op61 throw new System.InvalidOperationException($"unknown loot component '{stage.Component}'");62 }6364 // "unique" is item-bearing: unique-roll is still deferred, so a unique draw resolves as a normal item65 // roll until its slice lands (it refines ctx.Item rather than replacing the branch).66 var itemBearing = ctx.LootClass == "item" || ctx.LootClass == "unique";67 if (ItemOnly.Contains(stage.Component) && !itemBearing) continue;68 if (stage.Component == "currency-roll" && ctx.LootClass != "currency") continue;6970 component.Apply(ctx, rng, cfg, stage.Params);71 if (!ctx.Dropped)72 return new LootResult(false, null, null);73 }7475 // Enforce the LootResult invariant rather than only documenting it: a drop MUST carry something. Reachable76 // from authored data alone — e.g. a pipeline weighting `currency` but omitting the currency-roll stage would77 // otherwise return Dropped:true with no mints, silently dropping a reward on the floor.78 if (ctx.Dropped && ctx.Item == null && ctx.Currency == null)79 throw new System.InvalidOperationException(80 $"loot class '{ctx.LootClass}' dropped but produced neither an item nor currency — " +81 "the pipeline is missing the stage that resolves this class");8283 return new LootResult(ctx.Dropped, ctx.Item, ctx.Currency);84 }Toorn-shared039f617dotnet/Game.Kernel/Loot/LootPipeline.csLines 13 to 8455 lines
public sealed record LootResult(bool Dropped, RolledItem? Item, CurrencyDrop? Currency);
public static class LootPipeline
{
// Stateless components, safe to share across all ops (ADR-0020 component interpreter).
private static readonly IReadOnlyDictionary<string, ILootComponent> Registry =
new Dictionary<string, ILootComponent>
{
["gate"] = new GateComponent(),
["loot-class"] = new LootClassComponent(),
["rarity-roll"] = new RarityRollComponent(),
["type-roll"] = new TypeRollComponent(),
["class-roll"] = new ClassRollComponent(),
["item-level"] = new ItemLevelComponent(),
["mod-fill"] = new ModFillComponent(),
["currency-roll"] = new CurrencyRollComponent(),
["bonus-rolls"] = new BonusRollsComponent(),
};
// Class routing (SPEC-REWARD-SPINE §4): loot-class decides which branch's components apply. Skipping a
// component skips its rng draws too — both toolchains run this identical routing, so parity holds.
private static readonly HashSet<string> ItemOnly =
new HashSet<string> { "rarity-roll", "type-roll", "class-roll", "item-level", "mod-fill" };
private static readonly HashSet<string> Deferred =
new HashSet<string> { "unique-roll", "pity" };
public static LootResult Execute(ConfigPipeline pipeline, RollContext ctx, IRandom rng, KernelConfig cfg)
{
foreach (var stage in pipeline.Stages)
{
if (!Registry.TryGetValue(stage.Component, out var component))
{
if (Deferred.Contains(stage.Component)) continue; // valid but unimplemented in Slice 1 → no-op
throw new System.InvalidOperationException($"unknown loot component '{stage.Component}'");
}
// "unique" is item-bearing: unique-roll is still deferred, so a unique draw resolves as a normal item
// roll until its slice lands (it refines ctx.Item rather than replacing the branch).
var itemBearing = ctx.LootClass == "item" || ctx.LootClass == "unique";
if (ItemOnly.Contains(stage.Component) && !itemBearing) continue;
if (stage.Component == "currency-roll" && ctx.LootClass != "currency") continue;
component.Apply(ctx, rng, cfg, stage.Params);
if (!ctx.Dropped)
return new LootResult(false, null, null);
}
// Enforce the LootResult invariant rather than only documenting it: a drop MUST carry something. Reachable
// from authored data alone — e.g. a pipeline weighting `currency` but omitting the currency-roll stage would
// otherwise return Dropped:true with no mints, silently dropping a reward on the floor.
if (ctx.Dropped && ctx.Item == null && ctx.Currency == null)
throw new System.InvalidOperationException(
$"loot class '{ctx.LootClass}' dropped but produced neither an item nor currency — " +
"the pipeline is missing the stage that resolves this class");
return new LootResult(ctx.Dropped, ctx.Item, ctx.Currency);
}github.com/TrojanVoid60 repositories across 9 accounts, over 5.1 years. Most of it is private or owned by an organisation, which is why this page carries the figures.
Measured 2026-09-08