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.

74%of commit subjects carry a typefeat, fix, refactor
2files in the median commitsmall, reviewable
1:9test files to code files11,464 tests
23weeks in a row with a commitlongest unbroken run

Repositories started, per year

TypeScript 33%C# 31.9%JavaScript 11.4%HTML 10.3%Python 7.5%SCSS 2.5%Other 3.4%

Loot pipeline interpreterC#
    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);        }
**Adding a loot stage is authoring, not code.** The pipeline is data: a list of stage ids and their parameters, run here against a **registry of stateless components**. Most of the rest is about draw order. Client and server run this same routing over **one seeded generator**, so skipping a stage has to skip its draws too, and a no-drop returns before another is consumed. Stage names that are **locked but not yet built are no-ops**, and a name in neither set is a typo that throws. So does a drop that produced neither an item nor currency: authored data can reach that state, and the alternative is a reward landing quietly on the floor.

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