Blog / Ürün

Siparişten kargoya: üretim hattını yazılımla yönetmek

Fiziksel üretimi dijital akışa bağlamak: durum makineleri, bildirimler ve "siparişim nerede" sorusunun mühendisliği.

Bir matbaanın en gürültülü yeri baskı makineleri değildir; telefon santralidir. MatbaaStore'a başlamadan önce sahayı dinlediğimizde gelen çağrıların büyük çoğunluğu tek bir cümlenin varyasyonuydu: "Siparişim nerede?" Dosyam onaylandı mı, baskıya girdi mi, ne zaman kargolanır. Her çağrı bir operatörün işini bölüyor, her cevap birinin arka odaya gidip bir ustaya sormasıyla veriliyordu. Bu, bir müşteri hizmetleri problemi gibi görünür ama değildir. Bu bir ürün eksikliğidir.

Yıllar bize şunu öğretti: bir üretim hattı, siz onu modelleseniz de modellemeseniz de zaten bir durum makinesidir. Her sipariş her an tam olarak bir durumdadır — ya onay bekliyordur, ya baskı kuyruğundadır, ya paketlenmektedir. Soru bu durum makinesinin nerede yaşadığıdır: yazılımda mı, yoksa bir ustanın kafasında, duvardaki yapışkan notlarda ve WhatsApp gruplarında mı? İkinci seçenekte hat çalışır, ama ölçeklenemez ve müşteri asla gerçek zamanlı bilgiye ulaşamaz.

Bu yazıda fiziksel üretimi — onay, baskı, paketleme, kargo — nasıl açık bir durum makinesine döktüğümüzü anlatacağız. Her durum geçişinin nasıl bir olaya, her olayın nasıl bir müşteri bildirimine dönüştüğünü; tasarım onay döngüsünün neden en çok tıkanan yer olduğunu; baskı hatası ve iade gibi istisnaları nasıl ele aldığımızı; ve tüm bunları operasyon ekibinin gün boyu kullandığı panelin nasıl bir arada tuttuğunu göreceksiniz.

Üretim hattı zaten bir durum makinesi

Projenin başında en cazip yanlış, siparişe bir avuç bayrak eklemektir: onaylandiMi, basildiMi, paketlendiMi, kargolandiMi. İlk bakışta masum görünür. Ama bu dört boolean, on altı olası kombinasyon üretir ve bunların çoğu fiziksel dünyada anlamsızdır. kargolandiMi = true ama basildiMi = false olan bir sipariş nedir? Basılmadan kargolanmış bir ürün. Kod bunu temsil edebiliyorsa, er ya da geç bir hatalı güncelleme onu üretir ve siz bir müşteriye boş bir kutu göndermiş olursunuz.

Geçersiz bir durumun temsil edilebildiği her sistem, er ya da geç o geçersiz durumu üretir. İşin sırrı hatayı yakalamak değil, hatalı durumu baştan temsil edilemez kılmaktır.

Biz bunun yerine tek bir alan kullandık: durum. Bir siparişin aynı anda birden fazla durumda olması imkânsız; çünkü alan tek. Fiziksel hattı olduğu gibi haritaladık ve durumları müşterinin de operasyonun da anlayacağı isimlerle adlandırdık:

  • Onay bekliyor: sipariş alındı, ödeme geçti, ama tasarım dosyası henüz baskıya hazır değil.
  • Tasarım onayı: dosya yüklendi, baskı öncesi kontrolden geçti, müşterinin ya da bizim son onayımızı bekliyor.
  • Baskı kuyruğunda: onaylı, üretime alınmayı bekliyor; makine planına girecek.
  • Baskıda: fiilen baskı makinesinde ya da son işlemde (selefon, kesim, katlama).
  • Paketleme: baskı bitti, kalite kontrolü geçti, kutulanıyor.
  • Kargoda: kargo firmasına teslim edildi, takip numarası oluştu.
  • Teslim edildi: müşteriye ulaştı; hattın mutlu son durumu.
  • İptal / İade: akıştan çıkan sipariş; ayrıca ele alınması gereken terminal durumlar.

Bu liste sıradan görünür, ama asıl kıymeti durumların kendisinde değil, aralarındaki izin verilen geçişlerdedir. Onay bekleyen bir sipariş doğrudan kargoya atlayamaz. Baskıdaki bir sipariş onay durumuna geri dönemez — ama baskı hatası olursa kuyruğa geri dönebilir. Bu okları çizmek, veritabanı şemasını tasarlamaktan çok daha önemli bir iştir ve biz projeye tam olarak buradan, kağıt üzerinde durum-geçiş diyagramı çizerek başladık.

Bize sık sorulan bir soru şu: neden hazır bir iş akışı (workflow) motoru kullanmadınız? Piyasada durum makinelerini genel amaçlı yöneten kütüphaneler var. Denedik ve vazgeçtik. Bir matbaanın üretim hattı, on beş kutu ve elli okla ifade edilebilecek kadar küçük ve nettir; genel bir motorun getirdiği soyutlama katmanı, bu netliği okumayı zorlaştırmaktan başka işe yaramadı. Kendi durum makinemiz topu topu birkaç yüz satır Go; ama o birkaç yüz satırı ekipteki herkes on dakikada okuyup anlayabiliyor. Az kod, doğru kod olduğunda, en iyi kütüphanedir. Karmaşıklığı hak etmeyen bir probleme karmaşık bir araç sokmak, otuz yılın bize öğrettiği en pahalı alışkanlıklardan biridir.

Geçişi kod olarak yazmak

Durumları ve aralarındaki izinli geçişleri Go'da açık bir haritayla tanımladık. Bu harita, sistemin tek gerçeği: bir geçiş bu tabloda yoksa, o geçiş yasaktır. Kural koda gömülü bir "if" yığını değil, tek bakışta okunabilen bir veri yapısı olarak duruyor.

type Durum string

const (
    OnayBekliyor Durum = "onay_bekliyor"
    TasarimOnayi Durum = "tasarim_onayi"
    BaskiKuyrugu Durum = "baski_kuyrugu"
    Baskida      Durum = "baskida"
    Paketleme    Durum = "paketleme"
    Kargoda      Durum = "kargoda"
    TeslimEdildi Durum = "teslim_edildi"
    Iptal        Durum = "iptal"
)

// İzin verilen geçişler — bu haritada olmayan her geçiş hatadır
var gecisler = map[Durum][]Durum{
    OnayBekliyor: {TasarimOnayi, Iptal},
    TasarimOnayi: {BaskiKuyrugu, OnayBekliyor, Iptal}, // revizyon → geri
    BaskiKuyrugu: {Baskida, Iptal},
    Baskida:      {Paketleme, BaskiKuyrugu},           // baskı hatası → kuyruğa
    Paketleme:    {Kargoda},
    Kargoda:      {TeslimEdildi},
}

Durum değişikliği yalnızca tek bir fonksiyondan geçer. Hiçbir yerde siparis.Durum = "kargoda" gibi doğrudan atama yapılmaz; bu, hattın kuralsız bozulacağı ilk yerdir. Tek giriş kapısı hem geçişi doğrular hem de geçişi bir olaya çevirir:

func (s *Siparis) Gecir(hedef Durum, aktor string, sebep string) error {
    if !izinli(s.Durum, hedef) {
        return fmt.Errorf("gecersiz gecis: %s -> %s", s.Durum, hedef)
    }
    onceki := s.Durum
    s.Durum = hedef

    // Geçiş bir olaydır: denetim izi ve bildirim tek yerden akar
    return olayYaz(DurumDegisti{
        SiparisID: s.ID,
        Onceki:    onceki,
        Yeni:      hedef,
        Aktor:     aktor,   // hangi operatör ya da sistem
        Sebep:     sebep,   // geri geçişlerde zorunlu
        Zaman:     now(),
    })
}

Bu tasarımın ödülü, ilk günden çok sonra geldi. Geçiş mantığı tek yerde olduğu için, aylar sonra "kargoya geçerken otomatik fatura kes" ya da "baskıya girerken kağıt stoğunu düş" gibi kuralları eklemek tek bir fonksiyonu genişletmekten ibaret kaldı. Kuralları hattın her köşesine dağıtsaydık, aynı iş bir arkeoloji kazısına dönerdi. Tek yazar ilkesi — duruma yalnızca tek bir fonksiyonun dokunması — bu sistemin belkemiğidir.

Her geçiş bir olay, her olay bir bildirim

Durum makinesinin asıl gücü, geçişi bir "olay" olarak görmeye başladığınızda ortaya çıkıyor. Gecir fonksiyonu durumu değiştirmekle kalmaz, bir DurumDegisti olayı üretir. Bu olaydan iki şey türetiyoruz: değişmez bir denetim izi ve müşteriye giden bir bildirim. Sipariş baskı kuyruğuna girdiğinde müşteriye "Tasarımınız onaylandı, üretime alındı" gider; kargoya çıktığında takip numarasıyla birlikte "Siparişiniz yola çıktı" gider. Müşteri her adımı, sormasına gerek kalmadan öğrenir.

"Siparişim nerede?" çağrısı işte tam burada biter. Müşteri siparişin nerede olduğunu zaten biliyorsa aramaz. Sahaya baktığımızda gelen destek çağrılarının yarısına yakınının basit durum sorgusu olduğunu görmüştük; bildirimler ve müşterinin kendi hesabından görebildiği canlı durum çizelgesi devreye girdiğinde bu tür çağrılar bir avuca indi. Operasyon ekibi telefona cevap vermek yerine üretimi yönetmeye zaman ayırmaya başladı. Bu, yazılımın somut, ölçülebilir kazancıydı.

Ama burada acemi bir tuzak var: bildirimi geçişin tam ortasında, senkron olarak göndermek. HTTP isteğinin içinde SMS servisini çağırırsanız iki kötü şeyden biri olur. Ya durum kaydedilir ama SMS servisi patlar ve müşteri bilgilendirilmez; ya da SMS gider ama işlem geri alınır ve müşteri gerçekleşmeyen bir olayın bildirimini alır. Biz bunu outbox deseniyle çözdük: durum değişikliği ve olay kaydı aynı veritabanı işleminde, atomik olarak yazılır. Ayrı bir işçi (worker) bu olayları okuyup bildirimi gönderir ve her olayı yalnızca bir kez işlediğinden emin olur.

Durum değişikliği ile bildirim aynı işlemde ya birlikte olur ya hiç olmaz. Aralarına bir ağ çağrısı sıkıştırdığınız an, müşteriye yalan söyleme ihtimalini kod olarak yazmış olursunuz.

Bir başka ince nokta, bildirimlerin idempotent olması. İşçi bir olayı işleyip tam bittiğinde çökerse, yeniden başladığında aynı olayı görür. Her olaya benzersiz bir kimlik verip "bu kimlik daha önce gönderildi mi" kontrolü koyduk; böylece bir yeniden deneme, müşterinin telefonuna ikinci bir "siparişiniz kargoda" mesajı düşürmez. Küçük bir detay gibi görünür, ama güven bu tür detaylarda kazanılır ya da kaybedilir.

Bildirim kanallarını da müşteri tipine göre ayırdık. Bireysel müşteri için SMS ve e-posta yeterken, B2B tarafında — ajanslar, kurumsal alıcılar — durum bilgisinin çoğu zaman bir kişiye değil, bir cari hesaba akması gerekiyor. Bir ajansın on farklı işi aynı anda üretimdeyse, her birine ayrı ayrı SMS yağdırmak yardım değil gürültüdür. Onlara panellerinde toplu bir durum çizelgesi ve günlük özet verdik; kritik geçişlerde ise (kargoya çıkış gibi) yine anlık bildirim gitti. Doğru bildirim, en çok bildirim değildir; doğru kişiye, doğru anda, doğru ayrıntıda gidenidir. Bunu ayırt etmeyen sistemler, müşteriyi bilgilendirmek yerine bildirimlerini sessize almaya iter — ki o noktada altyapının tamamı boşa çalışır.

Tasarım onay döngüsü: en çok tıkanan yer

Mutlu yolu — sipariş gelir, basılır, kargolanır — tasarlamak kolaydır. Gerçek karmaşıklık, "onay bekliyor" ile "baskı kuyruğu" arasındaki bölgede saklıdır. Çünkü burası düz bir adım değil, bir döngüdür. Müşteri dosyasını yükler (PDF, AI ya da PSD), sistem baskı öncesi otomatik kontrolü yapar, bir dijital prova üretir; müşteri ya onaylar ya da revizyon ister. Revizyon isterse döngü başa döner. Bu döngü teoride sonsuza kadar dönebilir.

Baskı öncesi kontrol, bu döngünün en değerli parçası. Otomatik olarak taşma payını (bleed), çözünürlüğü, renk profilini (CMYK mı RGB mi) ve yazıların kenara fazla yaklaşıp kesime girip girmediğini denetliyoruz. Bu kontrol olmadan, bir hatayı ancak ürün basılıp elinize geldiğinde fark edersiniz — ki o noktada hata artık kağıt, mürekkep ve zaman kaybı demektir. Otomatik kontrol, en pahalı hataları henüz bedava düzeltilebilecekleri anda yakalar.

Döngünün tehlikesi, siparişlerin burada sessizce ölmesidir. Müşteri dosyayı yükler, revizyon ister, sonra unutur. Sipariş "onay bekliyor" durumunda haftalarca öylece durur. Bunu bir zamanlayıcıyla çözdük: onay bekleyen bir sipariş belirli bir süre hareketsiz kalırsa müşteriye hatırlatma gider, daha uzun sürerse sipariş otomatik olarak askıya alınır ve operasyon ekibinin dikkatine düşer. Sessiz ölümü, görünür bir duruma çevirdik.

Onayın kimden geldiği de düşündüğümüzden daha çetrefilliydi. Bireysel müşteride onay basittir: dosyayı yükleyen kişi provayı görür ve onaylar. Ama bir ajansta işi yükleyen tasarımcı ile onaya yetkili müşteri temsilcisi çoğu zaman farklı kişilerdir. Sistemi "yükleyen onaylar" varsayımıyla kursaydık, kurumsal akış ilk günden tıkanırdı. Bu yüzden onay yetkisini cari hesap düzeyinde tanımlanabilir kıldık: bir hesapta kim yükleyebilir, kim onaylayabilir ayrı ayrı belirlenir. Küçük bir esneklik gibi görünür, ama B2B tarafında bir siparişin akıp akmamasını tam olarak bu ayrıntı belirliyor.

Bir de bilinçli bir mimari karar verdik: onay bekleyen bir sipariş üretim kapasitesini işgal etmez. Tasarım onaylanmadan sipariş baskı kuyruğuna hiç girmez. Bu ayrım kritik, çünkü onaylanmamış işleri üretim planına karıştırırsanız kuyruk, gerçekte basılamayacak işlerle dolar ve makine planlaması yalan söylemeye başlar. Kuyrukta yalnızca gerçekten basılabilir, onaylı işler bulunur; kapasite hesabı böylece dürüst kalır.

İstisna yönetimi: baskı hatası ve iade

Bir sistemin olgunluğu mutlu yolda değil, işler ters gittiğinde belli olur. Baskı sırasında kağıt sıkışır, renk tutmaz, kesim kayar. Ürün kargoda hasar görür. Müşteri teslim aldıktan sonra iade ister. Bunların hiçbiri istisna değil, hattın rutin gerçekleridir; ve bunları sonradan yamamak yerine durum makinesinin doğal parçası olarak tasarlamak, bize çok baş ağrısından tasarruf ettirdi.

Baskı hatasını ele alalım. Acemi refleks, hatalı siparişi iptal edip yenisini açmaktır. Biz bunu yapmadık. Baskı hatası, siparişi Baskıda durumundan Baskı kuyruğu durumuna geri götüren meşru bir geçiştir — geçiş haritasında zaten tanımlı. Sipariş aynı sipariş kalır, müşteri numarası değişmez, ama başarısız baskı denemesi denetim izinde sonsuza dek kayıtlı kalır. Böylece "bu sipariş neden iki gün geç kaldı" sorusunun cevabı kayıptan değil, geçmişten okunur. Fire oranını da bu kayıtlardan ölçüyoruz; sağlıklı bir hatta yeniden baskı oranı yüzde birkaçı geçmez ve bu sayının tırmanması bir makine ya da tedarik sorununun erken habercisidir.

İlkemiz şuydu: durumu asla silme, üstüne yaz. Geçmiş, ekleme-yalnızca (append-only) bir kayıttır. Bir siparişin başına gelen her şey — her geçiş, her aktör, her sebep — kalıcıdır. Bir operatör siparişi yanlışlıkla bir adım ileri geçirdiğinde, onu geri alma da bir geçiştir ve kim, ne zaman, neden geri aldığı kaydedilir. Geri geçişler için sebep alanını zorunlu tuttuk; çünkü açıklamasız bir geri adım, sonradan hiç kimsenin çözemeyeceği bir muammaya dönüşür.

Kargo hasarı ayrı bir incelik getirdi. Ürün kusursuz basılıp doğru paketlendiği halde yolda ezilebilir; bu bizim hatamız değildir ama sorumluluğu bizimdir. Burada baskı hatasından farklı davrandık: sipariş kargoda hasar gördüğünde onu yeniden baskı kuyruğuna değil, yeni bir "yeniden üretim" işaretiyle kuyruğa aldık ve orijinal siparişe bağladık. Neden ayrım? Çünkü fire ölçümü yalan söylememeli. Baskı hatasını kendi makine firemize, kargo hasarını kargo firmasının performansına yazıyoruz; ikisini aynı kovaya atarsak, ne makinemizi ne tedarikçimizi dürüstçe değerlendirebiliriz. Ölçüm ancak sebepleri ayırdığınızda işe yarar.

İade ise ayrı bir konu. Kargoya çıkmış ya da teslim edilmiş bir sipariş, üretim döngüsüne geri dönemez — o iş çoktan yapılmıştır. İade, kendi terminal akışına sahip: iade talebi açılır, ürün geri gelir, muhasebe tarafında alacak/iade kaydı oluşur, gerekiyorsa stok düzeltilir. Bunu üretim durum makinesiyle karıştırmadık; çünkü iade, üretimin değil, ticaretin bir olayıdır. İki farklı yaşam döngüsünü tek bir dev makineye tıkmak, her ikisini de anlaşılmaz kılar. İyi bir durum makinesi, ne zaman ikinci bir makineye ihtiyaç olduğunu bilmekle de tanımlanır.

Operasyon ekibinin paneli

Şimdiye kadar müşteriden söz ettik, ama bu durum makinesinin asıl kullanıcısı müşteri değil, operasyon ekibidir. Onlar günün her saati bu makinenin içinde yaşar. Bu yüzden onlara bir yönetim ekranı değil, üretimi fiilen yönetebilecekleri bir komuta paneli yaptık. Panel, durumları sütunlara serilmiş bir kanban tahtası gibi çalışır: her sütun bir durum, her kart bir sipariş. Operatör bir işin fiziksel dünyada nerede olduğunu ekrana bakar bakmaz görür.

Kartları sürükle-bırakla değil, düğmelerle taşıttık. Bu bilinçli bir karardı: sürükle-bırak her yere bırakmaya izin verir, oysa bizim geçiş kurallarımız var. Panel yalnızca o durumdan gidilebilecek geçişleri düğme olarak gösterir; imkânsız bir geçiş arayüzde hiç belirmez. Kurallar hem sunucuda hem de operatörün gözünün önünde uygulanır. Panele bir de SLA sayaçları koyduk: bir sipariş baskıda olması gerekenden uzun süre beklerse kartı kırmızıya döner. Böylece tıkanan işler operatöre bağırır; operatörün onları avlaması gerekmez.

Matbaacılığın kendine has bir gerçeği de panele biçim verdi: işler tek tek basılmaz, toplu basılır. Aynı kağıda, aynı tabakaya kırk farklı müşterinin kartviziti birlikte dizilir. Dolayısıyla operatörün "bu tabakadaki tüm siparişleri baskıya al" ya da "hepsini paketlemeye geçir" diyebilmesi gerekir. Panele toplu geçiş yeteneği ekledik; tek tek tıklamak, günde yüzlerce sipariş işleyen bir ekip için kabul edilemez bir sürtünmedir. İç kullanıcı için ergonomi, müşteri arayüzü kadar önemlidir — çoğu ekip bunu unutur ve iç paneli bir sonradan-akla-gelen olarak bırakır.

Panelin bir de gerçek zamanlı olması gerekiyordu. İki operatör aynı anda çalışırken biri bir siparişi baskıya aldığında, diğerinin ekranında o kart hemen yer değiştirmeli; yoksa aynı işi iki kez üretmeye kalkarlar. Durum değişikliklerini Redis pub/sub üzerinden panele anlık ittik; ekran sayfa yenilemeden güncellenir. Binlerce siparişin içinde doğru olanı bulmak içinse Elasticsearch tabanlı arama devrede — müşteri adı, sipariş numarası ya da ürün tipiyle saniyede süzülür. Panel yavaşsa ekip onu kullanmaz, kenarda kendi Excel'ini tutar; ve o an durum makineniz gerçeği değil, kurguyu tutmaya başlar.

Sonuç

Siparişten kargoya uzanan fiziksel bir hattı yazılıma taşımanın özü, tek bir fikirde toplanıyor: süreci açık bir durum makinesi olarak modelleyin. Durumları adlandırın, aralarındaki izinli okları çizin, ve her geçişi tek bir kapıdan geçirin. O tek kapı, kuralların, denetim izinin ve müşteri bildiriminin ortak buluşma noktası olur. Bu disiplini kurduğunuzda, yeni bir kural eklemek koca sistemi elleşmek değil, tek bir fonksiyonu genişletmek olur.

Somut tavsiyemiz şu: projeye veritabanı şemasıyla değil, durum-geçiş diyagramıyla başlayın. Bir kağıda tüm durumları yazın, aralarında hangi geçişlerin mümkün olduğunu oklarla çizin ve geçersiz her durumu temsil edilemez kılın. Boolean bayrak yığınlarından kaçının; onlar imkânsız durumları mümkün kılar ve o imkânsız durumlar bir gün mutlaka başınıza gelir. Baskı hatasını, iadeyi, sessizce ölen onayları birer istisna olarak değil, makinenin baştan tasarlanmış birer geçişi olarak düşünün.

Ve son olarak, şu cümleyi bir pusula gibi tutun: bir siparişin nerede olduğunu müşteri size soruyorsa, sorun müşteri hizmetlerinizde değil, ürününüzdedir. İyi kurulmuş bir durum makinesi, o soruyu sorulmadan cevaplar — ve telefon santralini, matbaanın en gürültülü yeri olmaktan çıkarır.

Benzer bir altyapı mı kuruyorsunuz? Deneyimimizi projenize taşıyalım — 1 iş gününde teklif.
Teklif Al
yazilim.tech © 2026 yazilim.tech — Fikirden ürüne. Bir diji.tech markasıdır.