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> <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.
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.
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:
- Tahmin edilemezlik. "Bu ne kadar sürer?" sorusuna dürüst cevap "bilmiyorum"du. Bazen iki saat, bazen iki hafta — hangisi olacağını önceden görmenin bir yolu yoktu.
- Unpredictability. The honest answer to "how long will this take?" was "I don't know." Sometimes two hours, sometimes two weeks, with no way to tell in advance.
- Bilginin tek kişide toplanması. Her sistemin bir "o bilir" kişisi vardı. O kişi izne çıktığında ekibin hızı gözle görülür şekilde düşüyordu.
- Knowledge concentrated in one person. Each system had its "ask them" person. When that person took leave, the team's pace visibly dropped.
- Onboarding maliyeti. Yeni katılan bir mühendisin ilk anlamlı katkısı haftalar sonra geliyordu. Standart bir framework olsaydı günler sonra gelebilirdi.
- Onboarding cost. A new engineer's first meaningful contribution came weeks in. With a standard framework it could have come in days.
- Değişiklikten kaçınma. En sinsisi bu. Bir süre sonra ekip, teknik olarak doğru olan çözümü değil, dokunması en az korkutucu olan çözümü seçmeye başlıyor.
- Change avoidance. This is the insidious one. After a while the team stops picking the technically right solution and starts picking the one that's least frightening to touch.
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:
- Aynı veriyi görmeliler. İki sistem aynı siparişe baktığında aynı şeyi görmeli.
- They must see the same data. Looking at the same order, both systems must see the same thing.
- Aynı sözleşmeyi konuşmalılar. İstemci — mobil uygulama, web — bir isteğin hangi sistemden karşılandığını anlamamalı. Cevabın şekli aynı kalmalı.
- They must speak the same contract. The client — mobile app, web — must not be able to tell which system served a request. The shape of the response stays identical.
- Trafik geri alınabilmeli. Bir modülü yeniye almak tek yönlü bir kapı olmamalı; anahtarı geri çevirebilmeliyiz.
- Traffic must be reversible. Moving a module to the new system can't be a one-way door; we must be able to flip the switch back.
Üçü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:
- Sınırları net olmalı. "Bu iş kime ait?" sorusunun tartışmasız cevabı olan bir alan. Sınırı belirsiz bir modülü taşımaya kalkarsanız, taşıdıkça sınır büyür.
- Clear boundaries. An area where "whose job is this?" has an uncontested answer. Start moving a module with fuzzy edges and the edges grow as you move.
- Dışa bağımlılığı az olmalı. Sistemin yarısını çağıran bir modül, ilk modül olamaz.
- Few outward dependencies. A module that calls half the system can't be the first one.
- İş tarafına görünür değeri olmalı. Geçişin aylarca hiçbir şey üretmemesi, desteği kaybetmenin en hızlı yoludur.
- Visible value to the business. A migration that produces nothing for months is the fastest way to lose support for it.
- Geri alınabilir olmalı. Yanlış seçtiğimizi anlarsak tek bir anahtarla eskiye dönebilmeliyiz.
- Reversible. If we chose wrong, one switch should take us back.
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.
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:
- Gerçek yük altında nasıl davranacağını ölçmedik. Test ortamındaki sayılar, bir kampanya gününün sayıları değildir.
- We haven't measured how it behaves under real load. Numbers from a test environment are not the numbers of a campaign day.
- İki sistem paralel çalışırken veri tutarlılığının kenar durumları teoride çözülmüş durumda; sahada sınanmadı.
- The data-consistency edge cases of running two systems in parallel are solved in theory; they haven't been tested in the field.
- Geri alma senaryosunu tatbikat olarak çalıştırdık. Gerçek bir olay anında ne kadar pürüzsüz işleyeceğini ancak yaşayınca göreceğiz.
- We've run the rollback scenario as a drill. How smoothly it works during a real incident, we'll only learn by living it.
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.
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.
