Hakkımda

İstanbul'da backend geliştiricisiyim. Bir ürünün altında duran şeyi yazıyorum: gerçek zamanlı arka uçlar, çok kiracılı platformlar, onları besleyen hatlar ve ekibin hepsini içinden yönettiği paneller. Üç ürün, dördüncüsünün içinden çıkardığım bir substratın üzerinde duruyor. Oyun tarafında da istemciyi ve sunucuyu birlikte yazıyorum.

Nasıl çalışıyorum

Kararı yaz
Kod neyin yapıldığını söyler. Karar kaydı nedenini söyler, ve bir yıl sonra birinin ihtiyacı olan kısım odur. Genelde de o kararı geri alıp almamayı düşünürken. Sayıyı aşağıdaki kart, depoları tarayarak veriyor.
Yolun tamamını üstlen
Bir özellik pull request'te bitmiyor. Onu üç sınırdan geçirip en iyisini ummaktansa şemadan ekrana, oradan da yayına çıkaran deploy'a kadar kendim taşımayı tercih ederim.
Aracı yaz
Aynı iş elle iki kez yapıldıysa artık bir araçtır. Bir oyundaki denge hattı ile işteki operasyon paneli, farklı problemlere çevrilmiş aynı refleks.
Sıkıcı olanı seç
Problemi çözen en az sürpriz şeyi seçerim, kazandığım eforu da işin gerçekten zor olduğu yere harcarım.

Neyle geliştiriyorum

Oyun
İstemcide Unity ve C#, sunucuda .NET ve PostgreSQL, aralarında EF Core.
Ürün
PHP ve TypeScript, Slim ve NestJS, MySQL, PostgreSQL ve Redis. WebSocket daemon'ları HTTP uygulamasının içinde değil yanında duruyor. İş bir istek değil bir hat olduğunda Python'a geçiyorum.
Kurumsal
Bun üzerinde NestJS, Next.js, Postgres ve MySQL üzerinde Drizzle ile MikroORM, izinleri tutan CASL ve raporlamanın gerektirdiği her şey.

Birlikte çalışmak

Çalıştığım ekip dört mühendis. Üçü mobil istemcilerde, ben ağırlıkla arka uçtayım. Sesli sohbet platformunda gerçek zamanlı ses sağlayıcısını değiştirirken bu iş API'nin benim tarafımda bitmiyordu. O yüzden changelog gönderip beklemek yerine o migration boyunca onların deposuna, onların dalına commit attım.

Tek başıma
Bir işin tek mühendisi olmak, yazdığım dokümanın miktarını değil ne kadar erken yazıldığını değiştiriyor. Sonraki kişinin tek iş arkadaşı o doküman olacak, o yüzden gerekçe hâlâ tazeyken yazılıyor; bir çeyrek sonra hatırlanarak değil.

GitHub'da

Karar kaydı
1.011

Bir ADR yalnızca başkası okuyacağı için yazılır. Bunlar 38 depoya yayılıyor.

74%commit başlığı tür taşıyorfeat, fix, refactor
2dosya, ortanca commit'teküçük, gözden geçirilebilir
1:9test dosyası, kod dosyasına11.464 test
23hafta üst üste commiten uzun kesintisiz seri

Yıla göre açılan depo

TypeScript 33%C# 31.9%JavaScript 11.4%HTML 10.3%Python 7.5%SCSS 2.5%Diğer 3.4%

Loot pipeline yorumlayıcısıC#
    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);        }
**Yeni bir loot aşaması eklemek için kod değil veri yazıyorum.** Pipeline'ın kendisi veri olarak duruyor: aşama id'leri ve parametreleri. Bu sınıf onları **durumsuz bileşenlerden oluşan bir registry** üzerinden çalıştırıyor. Geri kalanın çoğu çekiliş sırasıyla ilgili. İstemci ile sunucu aynı yönlendirmeyi **tek bir tohumlanmış üreteç** üzerinde işletiyor, dolayısıyla atlanan bir aşamanın çekilişleri de atlanmak zorunda; hiçbir şey düşmediğinde fonksiyon fazladan çekiliş harcamadan dönüyor. **Kilitli ama henüz yazılmamış aşama adları no-op olarak geçiliyor**, iki kümede de bulunmayan bir ad ise yazım hatası sayılıp hata fırlatıyor. Düştüğü halde ne eşya ne para üreten bir sonuç da fırlatıyor, çünkü yazılan veri bu duruma gelebiliyor ve alternatifi ödülün sessizce yere düşmesi oluyor.

Toorn-shared039f617dotnet/Game.Kernel/Loot/LootPipeline.csSatır 13 – 8455 satır

    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/TrojanVoid5,1 yıl boyunca 9 hesapta 60 depo. Çoğu özel ya da bir kuruma ait; bu sayfa rakamları o yüzden taşıyor.

Ölçüm 2026-09-08