← Blog
MİMARİARCHITECTURE·LEGACY'DEN YENİYE · PART 1/3LEGACY TO NEW · PART 1/3·29 TEMMUZ 2026JUL 29, 2026·9 DK OKUMA9 MIN READ

Dokunmaya korktuğumuz kod: rewrite mi, kademeli geçiş mi?The code we were afraid to touch: rewrite, or migrate incrementally?

Sekiz yıldır kesintisiz çalışan bir e-ticaret sistemini yeniden yazmadan taşımaya karar verdik. Üç seçeneği — yerinde iyileştirme, sıfırdan yazma, kademeli devralma — dürüstçe konuştuk ve üçüncüsünü seçtik; çünkü tek bir üstünlüğü diğer her şeyi bastırıyordu: her adım geri alınabilir. Bu yazı sonuçların değil kararların anlatısı — henüz canlıda değiliz ve paylaşacak bir hız grafiğimiz yok.

We decided to migrate an eight-year-old, continuously running e-commerce system without rewriting it. We weighed three options honestly — improve in place, rewrite from scratch, take over incrementally — and picked the third, because one advantage outweighed everything else: every step is reversible. This is an account of decisions, not results — we're not live yet, and we have no speed chart to show you.

Naylalabs · MühendislikEngineering

01Tanıdık bir sahneA familiar scene

Şöyle bir sahne var; çoğu ekip kendi versiyonunu tanır.

There's a scene most teams will recognize a version of.

Ürün tarafından küçük bir istek gelir: "şu ekrana bir alan ekleyelim." Yarım saatlik iş gibi görünür. Dosyayı açarsınız. Dosya bin dört yüz satırdır. Aradığınız fonksiyon oradadır, ama aynı fonksiyon başka üç yerden daha çağrılıyordur. Çağıran yerlerden birinin adında temp geçer ve yanında dört yıl önceki bir tarih vardır. İşte o an, yarım saatlik işin yarım saatlik olmadığını anlarsınız.

Product asks for something small: "let's add a field to this screen." Looks like half an hour. You open the file. It's fourteen hundred lines. The function you need is there — but it's also called from three other places. One of those callers has temp in its name and a date from four years ago next to it. That's the moment you realize the half-hour job isn't a half-hour job.

Bu sahne kurgusal — belirli bir günü ya da olayı anlatmıyor. Ama uydurma da değil: legacy bir sistemle çalışan herkesin bir noktada yaşadığı andır. Bu yazı, biz bu anla ne yaptık, onu anlatıyor.

The scene is composed — it isn't a particular day or a particular incident. But it isn't invented either: it's the moment everyone working with a legacy system hits eventually. This post is about what we did with it.

Baştan netleştirelim: bu bir zafer turu değil. Yeni sistemimiz henüz canlıda değil. Elimizde "şu kadar hızlandık" diye paylaşacağımız bir grafik yok. Önce nasıl karar verdiğimizi anlatmak istiyoruz, çünkü işin asıl zor kısmı oydu.

Let's be clear up front: this is not a victory lap. Our new system isn't live yet. We have no "we got X% faster" chart to show. We want to describe how we decided first, because the deciding was the hard part.

02Elimizde ne vardıWhat we were working with

Sekiz yılı aşkın süredir kesintisiz çalışan, ciddi hacimli bir e-ticaret platformu. Arka tarafta iki ayrı sistem var.

An e-commerce platform running non-stop for more than eight years, at serious volume. Two separate systems behind it.

Yönetim paneli

The admin panel

Bir framework kullanmıyor — kendi framework'ü var. Kendi IoC container'ı, kendi konfigürasyon katmanı ve XML dosyalarında tanımlanmış bean'leri. İlk açtığımızda Spring'e benzettik. Ama PHP'ydi. Ve yazan kişi yıllar önce şirketten ayrılmıştı.

It doesn't use a framework — it has its own. Its own IoC container, its own configuration layer, and beans declared in XML files. When we first opened it, it reminded us of Spring. But it was PHP. And whoever wrote it had left the company years earlier.

beans.xml
<beans>
  <bean id="orderService"   class="core/service/orderservice.php" />
  <bean id="stockService"   class="core/service/stockservice.php" />
  <bean id="invoiceService" class="core/service/invoiceservice.php" />
</beans>

Temsili örnek. Bağımlılıklar XML'de tanımlı; hangi sınıfın nereden geldiğini görmek için kodu değil, konfigürasyonu okumanız gerekiyor.

Representative example. Dependencies live in XML; to see where a class comes from you read the configuration, not the code.

İstek akışı da kendine özgü: bütün URL'ler tek bir giriş dosyasına düşüyor, orada URL parçalanıyor ve dev bir switch bloğu hangi sayfanın yükleneceğine karar veriyor. Route tanımı diye bir şey yok; route, bir switch'in case'i.

Request flow is its own thing too: every URL lands in a single entry file, gets split apart, and a giant switch decides which page loads. There is no route definition; a route is a case in a switch.

index.php
switch ($url_array[0]) {
    case 'orders':
        $template     = 'orders';
        $include_page = 'order_list.php';
        break;
    case 'orders_detail':
        $template     = 'orders';
        $include_page = 'order_detail.php';
        break;
}

Temsili örnek — gerçekte bu blok yüzlerce case içeriyor. Yeni bir ekran eklemek, her seferinde buraya dokunmak demek.

Representative example — in reality this block holds hundreds of cases. Adding a screen means touching it every time.

Ortak bir veri erişim katmanı yok. Sorgular servis sınıflarının içinde parça parça kuruluyor ve aynı iş kuralı birbirinden habersiz birkaç yerde ayrı ayrı yazılmış durumda. Bir kuralı değiştirmek, önce onun kaç kopyası olduğunu bulmak demek. Otomatik test yok: bir değişikliğin doğru olduğunu anlamanın tek yolu ekranı açıp elle denemek.

There's no shared data-access layer. Queries are assembled piecemeal inside service classes, and the same business rule is written out in several places that don't know about each other. Changing a rule means first finding how many copies of it exist. There are no automated tests: the only way to know a change is correct is to open the screen and try it by hand.

Bir de script mezarlığı var: birbiriyle büyük ölçüde örtüşen iki dizin dolusu tek kullanımlık betik — export'lar, migration'lar, bir kere çalıştırılıp unutulmuş düzeltmeler. Hangisinin hâlâ gerektiğini kimse bilmiyor, bu yüzden hiçbiri silinmiyor. Loglama da kendi başına bir hikâye: merkezî toplama yok, format standart değil, geriye dönük arama pratikte mümkün değil.

And there's a script graveyard: two directories of largely overlapping one-off scripts — exports, migrations, fixes that ran once and were forgotten. Nobody knows which are still needed, so none get deleted. Logging is its own story: no central collection, no standard format, and searching backwards is not practically possible.

API

The API

İkinci sistem çok daha tanıdık: yaygın bir PHP framework'ü üzerine kurulmuş, düzgün bir proje. Sorun mimarisinde değil, yaşında. Framework'ün bugün geride kalmış bir sürümünde duruyor ve yukarı taşımak, bağımlılık zinciri yüzünden tek başına bir proje. İçinde yüzden fazla model, yan yana yaşayan birden fazla API versiyonu ve yıllar içinde her biri diğerinden bağımsız yazılmış entegrasyonlar var.

The second system is far more familiar: a decent project built on a common PHP framework. The problem isn't its architecture, it's its age. It sits on a version of the framework that's now well behind, and moving up is a project in itself because of the dependency chain. Inside: more than a hundred models, multiple API versions living side by side, and integrations each written independently of the others over the years.

Bir parantez.An aside. Bu kod şirketi bugüne taşıdı. Yukarıdaki her satır, bir noktada birinin gerçek bir sorunu çözmek için yazdığı, işe yaramış koddur — sekiz yıl boyunca sipariş aldı, para tahsil etti, kargo çıkardı, kampanya yetiştirdi. Legacy kod, kötü kod demek değildir; hayatta kalmış kod demektir. Bu yazının hiçbir cümlesi onu yazan ekibe karşı değil.This code carried the company to today. Every line above is code someone wrote to solve a real problem, and it worked — for eight years it took orders, collected money, shipped parcels, made campaign deadlines. Legacy code doesn't mean bad code; it means code that survived. Nothing in this post is against the team who wrote it.

03Asıl sorun kod değildiThe real problem wasn't the code

Legacy'yi tarif etmenin en yanıltıcı yolu "eski kod" demektir. Bizim işimize yarayan tanım şu oldu:

The most misleading way to describe legacy is "old code". The definition that actually served us:

Legacy, değiştirmekten korktuğumuz çalışan koddur.
Legacy is working code we're afraid to change.

Cümledeki iki kelime de önemli. Çalışan — yani bozuk değil, aksine iş görüyor. Korktuğumuz — yani sorun kodun kendisinde değil, kodla aramızdaki ilişkide. Bu korkunun somut, ölçülebilir sonuçları vardı:

Both words matter. Working — it isn't broken; it's doing the job. Afraid — the problem isn't in the code, it's in our relationship with it. That fear had concrete, measurable consequences:

Son maddeyi fark ettiğimiz gün işin aciliyeti değişti. Çünkü orada bir döngü var: korku kötü çözümleri seçtiriyor, kötü çözümler kodu daha da anlaşılmaz yapıyor, anlaşılmazlık korkuyu büyütüyor. Bu döngü kendi kendine yavaşlamıyor.

The day we noticed that last one, the urgency changed. Because there's a loop in it: fear drives bad choices, bad choices make the code harder to understand, and that opacity feeds the fear. The loop does not slow down on its own.

04Rewrite mi, refactor mı?Rewrite or refactor?

Masada üç seçenek vardı ve üçünü de dürüstçe konuştuk.

Three options were on the table, and we talked through all three honestly.

Yerinde iyileştirmeImprove in place Sıfırdan yazmaRewrite from scratch Kademeli devralmaIncremental takeover
RiskRisk DüşükLow Yüksek — tek seferde geçişHigh — one big cutover Orta, adım adım dağıtılmışMedium, spread across steps
İş devam eder mi?Does the business keep moving? EvetYes Hayır — geliştirme fiilen donarNo — development effectively freezes EvetYes
Geri alınabilir mi?Is it reversible? EvetYes Pratikte hayırNot in practice Evet — her modül tek tekYes — module by module
Çift bakım yüküDouble-maintenance burden YokNone Var, geçiş boyuncaYes, throughout Var, ama modül bazında sınırlıYes, but bounded per module
TavanıIts ceiling Temel varsayımlar değişmezCore assumptions never change Son %20 bitmezThe last 20% doesn't finish Yok — ama disiplin isterNone — but it demands discipline

Yerinde iyileştirme en ucuz ve en az riskli seçenek, ama net bir tavanı var: temel varsayımlar değişmiyor. Elle yazılmış framework elle yazılmış kalıyor, veri erişim biçimi aynı kalıyor, test edilebilirlik yapısal olarak sınırlı kalıyor. Beş yıl sonra aynı masada, aynı konuşmayı yapıyor oluruz — sadece biraz daha temiz bir versiyonuyla.

Improving in place is the cheapest, lowest-risk option, but it has a hard ceiling: the core assumptions don't change. The hand-rolled framework stays hand-rolled, data access stays as it is, testability stays structurally limited. Five years later we'd be at the same table having the same conversation, just with a slightly tidier version.

Sıfırdan yazmak kağıt üzerinde en cazip, tecrübeyle en tehlikeli seçenek — yirmi yılı aşkın süredir tekrarlanan bir uyarı. Üç sebeple eledik: iş duramaz (bir e-ticaret platformu bir yıl boyunca "yeni versiyonu bekliyoruz" diyemez); çift bakım (eski sistem durmayacağına göre her değişikliği iki yerde yapmak gerekir); ve son %20 bitmez — ilk %80 şaşırtıcı derecede hızlı gelir, kalanı kimsenin hatırlamadığı kenar durumlardır ve onlar eski kodun içinde dokümansız durur. Ancak canlıda çarptığınızda öğrenirsiniz.

Rewriting from scratch is the most attractive option on paper and the most dangerous in experience — a warning that's been repeated for over twenty years. We ruled it out for three reasons: the business can't stop (an e-commerce platform can't spend a year "waiting for the new version"); double maintenance (since the old system keeps running, every change has to be made twice); and the last 20% never finishes — the first 80% arrives surprisingly fast, and what remains are edge cases nobody remembers, sitting undocumented inside the old code. You learn them when they hit you in production.

Kademeli devralma yavaş, disiplin ister ve iki sistemi aynı anda ayakta tutmayı zorunlu kılar — ki bu başlı başına bir mühendislik problemidir. Ama tek bir üstünlüğü diğer her şeyi bastırdı: her adım geri alınabilir. Bir modülü yeni sisteme aldık ve beklediğimiz gibi davranmadı — trafiği geri alırız. Kaybettiğimiz bir hafta olur, bir yıl değil. Üçüncüsünü seçtik.

Incremental takeover is slow, demands discipline, and forces you to keep two systems alive at once — an engineering problem in its own right. But one advantage outweighed everything else: every step is reversible. We move a module to the new system and it doesn't behave as expected — we move the traffic back. We lose a week, not a year. We picked the third option.

05Boğan incir nedir?What is the strangler fig?

Bu yaklaşımın bir adı var: strangler fig — boğan incir. Desen ilk kez Martin Fowler tarafından 2004'te adlandırıldı; bugün Microsoft'un mimari kataloğu dahil pek çok yerde standart bir geçiş deseni olarak geçiyor.

The approach has a name: the strangler fig. The pattern was first named by Martin Fowler in 2004, and today it appears as a standard migration pattern in places including Microsoft's architecture catalogue.

Tropik ormanlarda bir incir türü, tohumunu başka bir ağacın dalına bırakır. Kökleri aşağı doğru sarkar, gövdeyi sarar ve yıllar içinde konak ağacın işlevlerini üstlenir. Konak ağaç sonunda çürüyüp yok olduğunda, incir kendi başına ayakta durur — ve içi boştur, çünkü artık oraya ihtiyacı yoktur.

In tropical forests, a fig drops its seed onto the branch of another tree. Its roots hang down, wrap the trunk, and over years take over the host's functions. When the host finally rots away, the fig stands on its own — hollow inside, because it no longer needs what was there.

Yazılımdaki karşılığı şu: yeni sistemi eskisinin önüne koyarsınız. Başlangıçta hemen her isteği eskiye devreder. Sonra bir modülü yeni sistem karşılamaya başlar. Sonra bir tane daha. Eski sistem küçülür. Bir gün geriye kimsenin çağırmadığı bir gövde kalır ve onu kaldırırsınız. Bunun tek ama zorlayıcı bir şartı var: iki sistemin aynı anda yaşayabilmesi. Pratikte üç şey demek:

The software version: you put the new system in front of the old one. At first it forwards almost every request backwards. Then the new system starts serving one module. Then another. The old system shrinks. One day what's left is a trunk nobody calls, and you remove it. There's a single, demanding precondition: the two systems must be able to live at the same time. In practice that means three things:

Üçüncüsü olmadan bu strateji, sadece daha yavaş bir "sıfırdan yazma"dır.

Without the third, this strategy is just a slower rewrite.

06İlk çizgiyi nereden çektikWhere we drew the first line

"Kademeli geçeceğiz" demek kolay; zor olan ilk parçayı seçmek. Bunun için dört kriter koyduk:

Saying "we'll migrate incrementally" is easy; choosing the first piece is the hard part. We set four criteria:

Bir de bilinçli olarak yapmadığımız iki şey var. İkisi de "doğru mimari" hevesiyle ilk gün yapılmaya çok müsait, ve ikisi de projeyi öldürebilirdi.

There are also two things we deliberately didn't do. Both are tempting on day one in the name of "proper architecture", and both could have killed the project.

Veritabanını ilk gün bölmedik

We didn't split the database on day one

Kitaplar her servisin kendi veritabanı olmasını söyler. Doğrudur — nihai hedefte. Ama iki sistem paralel çalışırken veriyi de bölerseniz, aynı anda hem yeni mimariyi hem de dağıtık veri tutarlılığını çözmeye çalışıyorsunuz demektir. Bunlar ayrı ayrı zor iki problem; birlikte çok daha zor. Yeni sistem bir süre eski şemayı okuyor. Bunun geçici bir taviz olduğunu ve bir gün ödeneceğini biliyoruz; şemayı ayırmak kendi başına bir iş kalemi.

The books say every service should own its database. True — as an end state. But if you split the data while two systems run in parallel, you're solving the new architecture and distributed data consistency at the same time. Those are two hard problems separately, and much harder together. For now the new system reads the old schema. We know it's a temporary concession that will be paid for one day; separating the schema is a work item of its own.

Frontend'e ilk gün dokunmadık

We didn't touch the frontend on day one

Mobil uygulama ve web, geçiş boyunca aynı sözleşmeyi görecek. Backend'i değiştirirken istemciyi de değiştirirseniz, bir sorun çıktığında hangisinden kaynaklandığını bilemezsiniz. Aynı anda iki değişken oynatmamaya çalıştık.

Mobile and web will see the same contract throughout the migration. If you change the client while you're changing the backend, then when something breaks you can't tell which one caused it. We tried not to move two variables at once.

07"Doğru mimari" derken neyi kastettikWhat we mean by "the right architecture"

Yeni sistemi TypeScript ve Node üzerine, Clean Architecture ve DDD prensipleriyle kurduk (runtime seçimini ayrıca tartışmıştık). Detayını Part 2'ye bırakıyoruz; üç cümlelik özü şu:

We built the new system on TypeScript and Node, following Clean Architecture and DDD principles (we've written separately about choosing a runtime). The detail goes in Part 2; the three-sentence version:

Bağımlılık oku tek yönlüdür. Dış katmanlar içe bakar; iç katman dışarıyı bilmez. İş kurallarının bulunduğu çekirdek, hangi veritabanını kullandığımızı ya da hangi HTTP kütüphanesini seçtiğimizi bilmez.

The dependency arrow points one way. Outer layers look inward; the inner layer knows nothing of the outside. The core, where the business rules live, doesn't know which database we use or which HTTP library we picked.

İş kuralları framework'ten bağımsızdır. "Bir sipariş hangi koşullarda iptal edilebilir?" sorusunun cevabı bir controller'ın içinde değil, tek başına okunabilen ve tek başına test edilebilen bir yerde durur.

Business rules are independent of the framework. The answer to "under what conditions can an order be cancelled?" doesn't live inside a controller; it lives somewhere you can read on its own and test on its own.

Port ve adapter, eskiye köprü kurmanın aracıdır. Geçiş açısından en kritik parça bu. Çekirdek "bana şu siparişi getir" der; nereden getirildiğini bilmez.

Ports and adapters are how you bridge to the old system. This is the critical piece for the migration. The core says "get me this order"; it doesn't know where it comes from.

order-repository.ts
export interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

export class LegacySchemaOrderRepository implements OrderRepository { /* ... */ }

export class NewSchemaOrderRepository implements OrderRepository { /* ... */ }

Temsili örnek. Aynı arayüzün iki implementasyonu: biri eski şemadan okur, diğeri yeniden.

Representative example. Two implementations of one interface: one reads the old schema, the other the new.

Hangisinin kullanılacağına, çekirdek kodda tek satır değiştirmeden karar verebiliriz. Kademeli geçişi mümkün kılan şey tam olarak bu. Yani buradaki mimari tercih bir estetik tercih değil: geçiş stratejisinin teknik ön koşulu.

We can decide which one is used without changing a line in the core. That is precisely what makes the incremental migration possible. So the architectural choice here isn't an aesthetic one: it's a technical precondition of the migration strategy.

08Henüz canlıda değilizWe're not live yet

Yazıyı bitirirken dürüst olalım. Bugün elimizde olan: kurulmuş bir mimari, çalışan modüller, dokümantasyon ve testler. Elimizde olmayan şey: gerçek trafik. Yani bilmediğimiz şeyler var — ve bunları bilmediğimizi biliyoruz:

Let's be honest as we close. What we have today: an architecture in place, working modules, documentation and tests. What we don't have: real traffic. So there are things we don't know — and we know that we don't know them:

Bunları yazmak, "şu kadar hızlandık" yazmaktan daha az tatmin edici. Ama bir mühendislik yazısının değeri, anlattığı şeyin ne kadar doğru olduğuyla ölçülür. Sonuçları paylaşacak duruma geldiğimizde, iyisiyle kötüsüyle paylaşacağız.

Writing this is less satisfying than writing "we got X% faster". But an engineering post is worth what its accuracy is worth. When we're in a position to share results, we'll share them — the good and the bad.

Kısa liste.Short list. Kademeli geçişin ön koşulları: iki sistem aynı veriyi görüyor ✓ · istemci hangi sistemin cevapladığını anlamıyor ✓ · trafik tek anahtarla geri alınabiliyor ✓ · ilk modülün sınırları net ve dışa bağımlılığı az ✓ · veritabanı ilk gün bölünmedi ✓ · istemci ile backend aynı anda değiştirilmedi ✓. Üçüncüsü yoksa elinizdeki şey kademeli geçiş değil, yavaşlatılmış bir rewrite'tır.Preconditions for an incremental migration: both systems see the same data ✓ · the client can't tell which one answered ✓ · traffic reverts with a single switch ✓ · the first module has clear boundaries and few dependencies ✓ · the database wasn't split on day one ✓ · client and backend weren't changed at the same time ✓. Without the third, what you have isn't an incremental migration — it's a rewrite in slow motion.

09Sık sorulanlarFrequently asked questions

Legacy sistem ne demek? İşimize yarayan tanım: değiştirmekten korktuğumuz çalışan kod. Sorun kodun yaşında değil, kodla aramızdaki ilişkide — legacy kod kötü kod değil, hayatta kalmış koddur.

What does "legacy system" mean? The definition that served us: working code we're afraid to change. The problem isn't the code's age but our relationship with it — legacy code isn't bad code, it's code that survived.

Rewrite ne zaman doğru karardır? Sistem gerçekten durdurulabiliyorsa, kapsam küçükse ve kenar durumlar dokümante edilmişse savunulabilir. Çalışan bir e-ticaret platformunda üçü de sağlanmaz.

When is a rewrite the right call? It's defensible when the system can genuinely be paused, the scope is small, and the edge cases are documented. On a live e-commerce platform, none of the three hold.

Veritabanını ne zaman bölmeli? İlk gün değil. Paralel çalışan iki sistemde veriyi de bölmek, yeni mimariyle dağıtık veri tutarlılığını aynı anda çözmeye çalışmak demektir. Şemayı ayırmak sonraya bırakılan ayrı bir iş kalemidir.

When should you split the database? Not on day one. Splitting the data while two systems run in parallel means solving the new architecture and distributed consistency simultaneously. Separating the schema is a separate work item, deferred on purpose.

İki sistemi aynı anda ayakta tutmak pahalı değil mi? Evet, ve bu maliyeti bilerek kabul ettik. Alternatifin maliyeti — bir yıl boyunca donmuş bir ürün ve geri alınamayan tek bir geçiş anı — daha yüksekti.

Isn't keeping two systems alive expensive? Yes, and we accepted that cost knowingly. The alternative's cost — a frozen product for a year and a single cutover you can't undo — was higher.

Testi olmayan bir sistemde nereden başlanır? Sınırları net, dışa bağımlılığı az ve geri alınabilir bir modülden. Test yokluğu bu kriterleri gevşetmez; tam tersine daha da zorunlu kılar.

Where do you start when there are no tests? With a module that has clear boundaries, few outward dependencies, and a way back. The absence of tests doesn't relax those criteria — it makes them mandatory.

LegacyStrangler FigClean ArchitectureDDD

Serinin devamı. Part 2 — yeni backend'in anatomisi: katmanlar, port ve adapter'ların gerçekte nasıl yazıldığı, bir modülün klasör yapısı. Part 3 — ilk modülün taşınması: eski kodun hali, yeni haldeki karşılığı, aradaki köprü ve trafiği kaydırırken yaptığımız hatalar. · Metinde şirket, proje ve modül adı geçmiyor; kod örnekleri temsilidir. Coming in this series. Part 2 — the anatomy of the new backend: layers, how ports and adapters are actually written, a module's folder structure. Part 3 — migrating the first module: what the old code looked like, its new counterpart, the bridge between them, and the mistakes we made shifting traffic. · No company, project or module names appear in this text; code samples are representative.