<?xml version="1.0" encoding="utf-8"?>
<feed xmlns:blogChannel="http://backend.userland.com/blogChannelModule" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:pingback="http://madskills.com/public/xml/rss/module/pingback/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" xmlns:betag="http://dotnetblogengine.net/schemas/tags" xmlns="http://www.w3.org/2005/Atom">
  <id>https://www.buraksenyurt.com/</id>
  <title>Burak Selim Şenyurt</title>
  <updated>2026-09-07T16:00:41+00:00</updated>
  <link href="https://www.buraksenyurt.com/" />
  <link rel="self" href="https://www.buraksenyurt.com/syndication.axd?format=atom" />
  <subtitle>Matematik Mühendisi Bir Bilgisayar Programcısının Notları</subtitle>
  <author>
    <name>Burak Selim Senyurt</name>
  </author>
  <generator uri="http://dotnetblogengine.net/" version="1.0.0.0">BlogEngine.Net Syndication Generator</generator>
  <blogChannel:blogRoll>https://www.buraksenyurt.com/opml.axd</blogChannel:blogRoll>
  <dc:creator>Burak Selim Senyurt</dc:creator>
  <dc:description>Matematik Mühendisi Bir Bilgisayar Programcısının Notları</dc:description>
  <dc:language>tr-TR</dc:language>
  <dc:title>Burak Selim Şenyurt</dc:title>
  <geo:lat>0.000000</geo:lat>
  <geo:long>0.000000</geo:long>
  <entry>
    <id>https://www.buraksenyurt.com/post/testler-icin-bir-mail-sunucusu-yazmak</id>
    <title>Testler için Bir Mail Sunucusu Yazmak</title>
    <updated>2026-09-07T10:52:00+00:00</updated>
    <link rel="self" href="https://www.buraksenyurt.com/post.aspx?id=279d0ee3-e679-496b-8873-7dd6f5445828" />
    <link href="https://www.buraksenyurt.com/post/testler-icin-bir-mail-sunucusu-yazmak" />
    <author>
      <name>bsenyurt</name>
    </author>
    <summary type="html">&lt;p&gt;Uzun zaman &amp;ouml;nce &lt;strong&gt;Friday Night Programmer&lt;/strong&gt; laboratuvarında bir e-posta sunucusuna ihtiyacım oldu. Amacım, belli bir s&amp;uuml;recin entegrasyon testleri sırasında bir e-posta sunucusuna gidip gelme aksiyonunu taklit etmekti. İhtiyacım olan entegrasyon testi, ortamda ger&amp;ccedil;ek bir e-posta sunucusu olmadan da &amp;ccedil;alışabilmeliydi. Nitekim asıl odak noktam e-postaların gidip gitmemesi değildi. S&amp;uuml;recin bir par&amp;ccedil;ası olarak ilgili e-postalar gitmiş ve sunucudan olumlu cevap almışım&lt;em&gt;(ya da almamışım)&lt;/em&gt; gibi hareket etmek istiyordum. Pekala, bu konuda yazılmış &amp;ccedil;eşitli sahte&lt;em&gt;(fake)&lt;/em&gt; sunucular olabilirdi ama bana hafif sıklet bir aparat gerekiyordu. Aklıma &lt;strong&gt;Rust&lt;/strong&gt; ve bu tip sunucu bazlı işlemlerde &amp;ouml;ne &amp;ccedil;ıkan tokio&lt;strong&gt; k&amp;uuml;fesi&lt;em&gt;(crate)&lt;/em&gt;&lt;/strong&gt; geldi. Sonu&amp;ccedil;ta bir sunucu, belli bir port &amp;uuml;zerinden gelen mesajları yakalayıp şakacıktan karşı tarafa g&amp;ouml;nderdim gibi yapsa yeterliydi. Bu yazıdaki amacım, &amp;ouml;rnek kodlar &amp;uuml;zerinden ge&amp;ccedil;erek kendime ve sizlere hatırlatıcı noktalar &amp;ccedil;ıkarmak olacak. &amp;Ouml;yleyse gelin vakit kaybetmeden başlayalım.&lt;/p&gt;
&lt;h2&gt;Mail Sunucuları Hakkında Genel Bilgi&lt;/h2&gt;
&lt;p&gt;Pekala, bir e-posta sunucusu bir&amp;ccedil;ok standardı b&amp;uuml;nyesinde barındırır. Gelen postalar i&amp;ccedil;in &lt;strong&gt;IMAP&lt;/strong&gt;&lt;em&gt;(Internet Message Access Protocol)&lt;/em&gt;, &lt;strong&gt;POP3&lt;/strong&gt;&lt;em&gt;(Post Office Protocol 3)&lt;/em&gt; gibi protokoller s&amp;ouml;z konusuyken, giden posta tarafında &lt;strong&gt;SMTP&lt;/strong&gt;&lt;em&gt;(Simple Mail Transfer Protocol)&lt;/em&gt; devreye girer. En başından beri bu protokollerin baz aldığı 25, 465, 587, 2525, 110, 143, 993 vb. portlar da s&amp;ouml;z konusudur.&lt;br /&gt;&lt;br /&gt;Bu &amp;ccedil;alışmadaki amacım ger&amp;ccedil;ek anlamda kullanılabilir ve &amp;uuml;retim&lt;em&gt;(production)&lt;/em&gt; ortamına konabilecek bir e-posta sunucusu yazmak değil elbette. Entegrasyon testleri sırasında &lt;strong&gt;SMTP&lt;/strong&gt; protokol&amp;uuml;n&amp;uuml;n temel bazı komutlarını &lt;strong&gt;HELO/EHLO, MAIL FROM, RCPT TO, DATA, QUIT&lt;/strong&gt; taklit eden, gelen mesajları diske yazan ve gerektiğinde belirli senaryolarda hata da d&amp;ouml;nebilen hafif sıklet&lt;em&gt;(lightweight)&lt;/em&gt; bir sahte&lt;em&gt;(fake)&lt;/em&gt; sunucu işimi g&amp;ouml;r&amp;uuml;yor.&lt;em&gt;(Hata d&amp;ouml;nmesi sunucudan istenen cevap alınamadığında sistemin kalan kısmının nasıl davranış g&amp;ouml;stereceğini anlamak a&amp;ccedil;ısından da &amp;ouml;nemli olabilir)&lt;/em&gt; B&amp;ouml;ylece test ortamında ger&amp;ccedil;ek bir &lt;strong&gt;SMTP&lt;/strong&gt; sağlayıcısına bağımlı kalmadan, e-posta g&amp;ouml;nderim akışının u&amp;ccedil;tan uca doğru &amp;ccedil;alıştığını doğrulayabiliyordum.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tabii ki tam teşekk&amp;uuml;ll&amp;uuml; bir SMTP sunucusu yazmayı da d&amp;uuml;ş&amp;uuml;nebiliriz. Bu durumda protokol&amp;uuml;n rfc5321 standardına uygun olarak t&amp;uuml;m komutları ve hata durumlarını ele almamız gerekecektir. Bakınız &lt;a href="https://datatracker.ietf.org/doc/html/rfc5321" target="_blank"&gt;RFC 5321&lt;/a&gt;. Bizim demomuzda &amp;ccedil;ok basit birka&amp;ccedil; komutu ele alıyoruz.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Gerekli K&amp;uuml;felerin Y&amp;uuml;klenmesi&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Rust&lt;/strong&gt; projesini&lt;em&gt;(executable)&lt;/em&gt; oluşturduktan sonra gerekli crate eklemelerini yaparak işe başlayalım. &amp;Ouml;rnek kodları yazdığım tarih itibariyle&lt;em&gt;(2025 ortaları gibi)&lt;/em&gt; &lt;strong&gt;toml&lt;/strong&gt; i&amp;ccedil;eriği aşağıdaki gibiydi. Paket s&amp;uuml;r&amp;uuml;mlerinde g&amp;uuml;ncellemeler olduğunu s&amp;ouml;yleyebilirim. Buna g&amp;ouml;re son s&amp;uuml;r&amp;uuml;mleri ile de &amp;ccedil;alışabilirsiniz.&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;[package]
name = "light_mail_server"
version = "0.1.0"
edition = "2021"

[dependencies]
env_logger = "0.11.6"
log = "0.4.25"
tokio = { version = "1.43.0", features = ["full"] }&lt;/pre&gt;
&lt;p&gt;En &amp;ouml;nemli mod&amp;uuml;l&amp;uuml;m&amp;uuml;z asenkron kabiliyetleri ile &amp;ouml;ne &amp;ccedil;ıkan &lt;strong&gt;tokio&lt;/strong&gt; k&amp;uuml;fesi&lt;em&gt;(crate)&lt;/em&gt;. Bunun haricinde sunucu tarafı i&amp;ccedil;in log bırakmak da iyi olacaktır; nitekim uygulamayı bir &lt;strong&gt;Docker&lt;/strong&gt; &lt;strong&gt;container&lt;/strong&gt;'ı olarak ayağa kaldırdığımızda terminal loglarından durumu inceleme şansımız da olur. &lt;strong&gt;log&lt;/strong&gt; ve &lt;strong&gt;env_logger&lt;/strong&gt; k&amp;uuml;felerini bu ama&amp;ccedil;la kullanıyoruz. Bu k&amp;uuml;feleri doğrudan &lt;strong&gt;toml&lt;/strong&gt; dosyasına yazabileceğiniz gibi aşağıdaki terminal komutları ile de ekleyebiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;cargo add env_logger, log
cargo add tokio -F full&lt;/pre&gt;
&lt;h2&gt;İhtiyacımız Olan Bazı Sabitler ve Utility&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;SMTP&lt;/strong&gt; protokol&amp;uuml;n&amp;uuml; bir nebze taklit edeceğimiz i&amp;ccedil;in olaya dahil olan komutları ve sunucunun vereceği cevapları bir &lt;strong&gt;enum&lt;/strong&gt; t&amp;uuml;r&amp;uuml;nde toplamayı tercih ettim. &lt;strong&gt;OCaml&lt;/strong&gt; genlerinden gelen zengin bir enum t&amp;uuml;r&amp;uuml; var, her fırsatta s&amp;ouml;ylemek istiyorum.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use std::fmt::{Display, Formatter};

pub enum Command {
    Hello,
    Hello2,
    HelloResponse,
    From,
    To,
    Data,
    Bye,
    Ok,
    Quit,
    Unrecognized,
    EndDataReply,
    ServiceNotAvailable,
    ServerReady,
    MessageAccepted,
    ErrorSimulated,
}

impl Command {
    pub fn as_str(&amp;amp;self) -&amp;gt; &amp;amp;'static str {
        match self {
            Command::Hello =&amp;gt; "HELO",
            Command::Hello2 =&amp;gt; "EHLO",
            Command::HelloResponse =&amp;gt; "250 Hello\r\n",
            Command::From =&amp;gt; "MAIL FROM",
            Command::To =&amp;gt; "RCPT TO",
            Command::Data =&amp;gt; "DATA",
            Command::Bye =&amp;gt; "221 Bye\r\n",
            Command::Ok =&amp;gt; "250 OK\r\n",
            Command::Quit =&amp;gt; "QUIT",
            Command::Unrecognized =&amp;gt; "500 Unrecognized command\r\n",
            Command::EndDataReply =&amp;gt; "354 End data with &amp;lt;CR&amp;gt;&amp;lt;LF&amp;gt;.&amp;lt;CR&amp;gt;&amp;lt;LF&amp;gt;\r\n",
            Command::ServiceNotAvailable =&amp;gt; "421 Service Not Available\r\n",
            Command::ServerReady =&amp;gt; "220 Mock SMTP Server Ready\r\n",
            Command::MessageAccepted =&amp;gt; "250 Message accepted for delivery\r\n",
            Command::ErrorSimulated =&amp;gt; "421 Error simulated for recipient.",
        }
    }
}

impl Display for Command {
    fn fmt(&amp;amp;self, f: &amp;amp;mut Formatter&amp;lt;'_&amp;gt;) -&amp;gt; std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

pub const BUFFER_SIZE: usize = 1024;
pub const EMAIL_DIR: &amp;amp;str = "temp";
pub const INVALID_MAIL: &amp;amp;str = "thereisnomail@nowhere.nowhere";
pub const ENDS_WITH: &amp;amp;str = "\r\n.\r\n";
pub const NO_BUFF_OR_EOF:usize = 0;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Command&lt;/strong&gt; &lt;strong&gt;enum&lt;/strong&gt; yapısı hem istemciden gelmesini beklediğimiz komutlar&lt;em&gt;ı(HELO/EHLO, MAIL FROM, RCPT TO, DATA, QUIT)&lt;/em&gt; hem de sunucunun geri d&amp;ouml;neceği cevaplar&lt;em&gt;ı(220, 250, 354, 421, 500 gibi klasik SMTP durum kodlarını)&lt;/em&gt; aynı tip altında topluyor. &lt;strong&gt;as_str()&lt;/strong&gt; her varyanta karşılık gelen ham metni d&amp;ouml;nd&amp;uuml;r&amp;uuml;yor, &lt;strong&gt;Display&lt;/strong&gt; &lt;strong&gt;trait&lt;/strong&gt;'i de bu &lt;strong&gt;string'i {}&lt;/strong&gt; ile doğrudan formatlayabilmemizi sağlıyor. B&amp;ouml;ylece sunucu tarafında &lt;strong&gt;"hangi duruma hangi metni yazacağız"&lt;/strong&gt; sorusunu enum &amp;uuml;zerinden, tip g&amp;uuml;venli&lt;em&gt;(type safe)&lt;/em&gt; bir şekilde &amp;ccedil;&amp;ouml;zm&amp;uuml;ş oluyoruz. Kodun kalan kısmında yer alan sabitlerin kullanım ama&amp;ccedil;ları ise aşağıdaki gibi.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BUFFER_SIZE&lt;/strong&gt; soket okumalarında kullanılacak tampon boyutu.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EMAIL_DIR&lt;/strong&gt; gelen e-postaların diske yazılacağı klas&amp;ouml;r.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ENDS_WITH&lt;/strong&gt; ise &lt;strong&gt;SMTP&lt;/strong&gt; protokol&amp;uuml;nde &lt;strong&gt;DAT&lt;/strong&gt;A komutunun bitişini işaret eden &lt;strong&gt;\r\n.\r\n&lt;/strong&gt; dizisini temsil ediyor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INVALID_MAIL&lt;/strong&gt;, testlerde bilerek hataya d&amp;uuml;ş&amp;uuml;rmek istediğimiz senaryolar i&amp;ccedil;in ayırdığımız &amp;ouml;zel bir alıcı adresi. Sunucu bu adresi g&amp;ouml;rd&amp;uuml;ğ&amp;uuml;nde normal akışı bozup &lt;strong&gt;421&lt;/strong&gt; hatasını sim&amp;uuml;le ediyor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NO_BUFF_OR_EOF&lt;/strong&gt; değeri socket.read'in 0 d&amp;ouml;nd&amp;uuml;ğ&amp;uuml;&lt;em&gt;(yani karşı tarafın bağlantıyı kapattığı)&lt;/em&gt; durumu isimlendirerek kodun okunabilirliğini artırıyor.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;İhtiya&amp;ccedil; duyabileceğimiz &amp;ccedil;ok basit iki fonksiyonu ise&lt;strong&gt; utility.rs&lt;/strong&gt; isimli dosyada tutmayı tercih ettim.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use crate::constants::*;
use log::*;
use std::fs::OpenOptions;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;

pub fn save_email(body: &amp;amp;str) -&amp;gt; std::io::Result&amp;lt;()&amp;gt; {
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    let filename = format!("{}/email_{}.txt", EMAIL_DIR, timestamp);

    let mut file = OpenOptions::new()
        .create(true)
        .truncate(true)
        .write(true)
        .append(false)
        .open(&amp;amp;filename)?;

    file.write_all(body.as_bytes())?;
    info!("Email saved to {}", filename);

    Ok(())
}

pub async fn send_response(socket: &amp;amp;mut TcpStream, command: Command) -&amp;gt; tokio::io::Result&amp;lt;()&amp;gt; {
    socket
        .write_all(command.as_str().as_bytes())
        .await
        .map_err(|e| {
            error!("Failed to send response '{}': {}", command.as_str(), e);
            e
        })
}&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;save_email&lt;/strong&gt; fonksiyonu, &lt;strong&gt;DATA&lt;/strong&gt; bloğunda toplanan e-posta i&amp;ccedil;eriğini alıp &lt;strong&gt;temp/email_&amp;lt;unix_timestamp&amp;gt;.txt&lt;/strong&gt; formasyonunda isimlendirerek metin tabanlı bir dosyaya kaydediyor. B&amp;ouml;ylece testler sırasında ger&amp;ccedil;ekten ne g&amp;ouml;nderilmiş sorusunun cevabını sunucuya hi&amp;ccedil; dokunmadan diskten okuyabiliriz&lt;em&gt;(Tabii diske yazma yetkisine sahip olunması gerekir)&lt;/em&gt; &lt;strong&gt;send_response&lt;/strong&gt; fonksiyonu ise asenkron tarafta tekrar tekrar ele alacağımız &lt;strong&gt;"soket'e bir Command cevabı yaz"&lt;/strong&gt; işlemini tek bir yerden kontrol etmek i&amp;ccedil;in kullanılıyor. Hata durumunda ise log &amp;uuml;zerinden bir error! satırı bırakıp hatayı &amp;ccedil;ağırana geri d&amp;ouml;nd&amp;uuml;r&amp;uuml;yor. Bu iki fonksiyonu kullandığımız yerleri g&amp;ouml;r&amp;uuml;nce kafanızda daha net canlanacaktır diye d&amp;uuml;ş&amp;uuml;n&amp;uuml;yorum.&lt;/p&gt;
&lt;h2&gt;Sunucu Tarafı&lt;/h2&gt;
&lt;p&gt;Sunucu g&amp;ouml;revini &lt;strong&gt;Server&lt;/strong&gt; isimli bir &lt;strong&gt;struct&lt;/strong&gt; karşılıyor. Tabii bu metodun bir &lt;strong&gt;TcpStream&lt;/strong&gt; &amp;uuml;zerinde &amp;ccedil;alışması ve geriye &lt;strong&gt;tokio&lt;/strong&gt; &amp;ccedil;alışma zamanının kullanabileceği bir Result nesnesi d&amp;ouml;nd&amp;uuml;rmesi gerekiyor.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use crate::constants::*;
use crate::utility::*;
use log::*;
use std::io::ErrorKind;
use tokio::io::AsyncReadExt;
use tokio::net::TcpStream;

pub struct Server {}

impl Server {
    pub async fn handle(socket: &amp;amp;mut TcpStream) -&amp;gt; tokio::io::Result&amp;lt;()&amp;gt; {
        send_response(socket, Command::ServerReady).await?;

        let mut buffer = [0; BUFFER_SIZE];
        let mut email_body = String::new();
        let mut data_mode = false;

        loop {
            match socket.read(&amp;amp;mut buffer).await {
                Ok(NO_BUFF_OR_EOF) =&amp;gt; {
                    info!("Connection closed by client");
                    break;
                }
                Ok(n) =&amp;gt; {
                    let request = String::from_utf8_lossy(&amp;amp;buffer[..n]);
                    info!("Received: {}", request);

                    if data_mode {
                        email_body.push_str(&amp;amp;request);

                        if email_body.ends_with(ENDS_WITH) {
                            let clean_body = email_body.trim_end_matches(ENDS_WITH);
                            if let Err(e) = save_email(clean_body) {
                                error!("Failed to save email: {}", e);
                            }
                            email_body.clear();

                            send_response(socket, Command::MessageAccepted).await?;
                            data_mode = false;
                        }
                    } else {
                        match request.trim() {
                            r if r.starts_with(Command::Hello.as_str())
                                || r.starts_with(Command::Hello2.as_str()) =&amp;gt;
                            {
                                send_response(socket, Command::HelloResponse).await?;
                            }
                            r if r.starts_with(Command::From.as_str()) =&amp;gt; {
                                send_response(socket, Command::Ok).await?;
                            }
                            r if r.starts_with(Command::To.as_str()) =&amp;gt; {
                                if r.contains(INVALID_MAIL) {
                                    send_response(socket, Command::ServiceNotAvailable).await?;
                                    error!("{}", Command::ErrorSimulated.as_str());
                                } else {
                                    send_response(socket, Command::Ok).await?;
                                }
                            }
                            r if r.starts_with(Command::Data.as_str()) =&amp;gt; {
                                send_response(socket, Command::EndDataReply).await?;
                                data_mode = true;
                            }
                            r if r.starts_with(Command::Quit.as_str()) =&amp;gt; {
                                send_response(socket, Command::Bye).await?;
                                info!("Session closed");
                                break;
                            }
                            _ =&amp;gt; {
                                send_response(socket, Command::Unrecognized).await?;
                                error!("Unrecognized command received: {}", request);
                            }
                        }
                    }
                }
                Err(e) if e.kind() == ErrorKind::ConnectionReset =&amp;gt; {
                    warn!("Connection reset by peer");
                    break;
                }
                Err(e) =&amp;gt; {
                    error!("Error reading from socket: {}", e);
                    break;
                }
            }
        }

        Ok(())
    }
}&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Server::handle&lt;/strong&gt; fonksiyonunu tek bir &lt;strong&gt;TCP&lt;/strong&gt; bağlantısının t&amp;uuml;m yaşam &amp;ouml;mr&amp;uuml;n&amp;uuml; y&amp;ouml;neten bir metot olarak d&amp;uuml;ş&amp;uuml;nebiliriz. Bağlantı geldiği anda &amp;ouml;nce &lt;strong&gt;220 Mock SMTP Server Ready&lt;/strong&gt; cevabıyla kendini tanıtıyor&lt;em&gt;(tıpkı ger&amp;ccedil;ek bir SMTP sunucusunun yaptığı gibi)&lt;/em&gt;, sonra sonsuz bir d&amp;ouml;ng&amp;uuml;de soketten veri okumaya devam ediyor. Burada iki mod arasında gidip geliyoruz. data_mode false iken gelen her satırı bilinen &lt;strong&gt;SMTP&lt;/strong&gt; komutlarıyla&lt;em&gt;(HELO/EHLO, MAIL FROM, RCPT TO, DATA, QUIT)&lt;/em&gt; eşleştirip uygun cevabı d&amp;ouml;n&amp;uuml;yoruz. &lt;strong&gt;DATA&lt;/strong&gt; komutu geldiğinde ise data_mode değerini true yapıp asıl e-posta g&amp;ouml;vdesini&lt;em&gt;(body)&lt;/em&gt; toplamaya başlıyoruz. G&amp;ouml;vde, protokol&amp;uuml;n bitiş işareti olan&lt;strong&gt; \r\n.\r\n&lt;/strong&gt; dizisiyle karşılaşana kadar email_body i&amp;ccedil;inde biriktiriliyor, karşılaşınca da save_email ile diske yazılıp &lt;strong&gt;250 Message accepted for delivery&lt;/strong&gt; cevabı d&amp;ouml;n&amp;uuml;l&amp;uuml;yor.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;RCPT TO&lt;/strong&gt; adımında ayrıca k&amp;uuml;&amp;ccedil;&amp;uuml;k bir &lt;strong&gt;"hata sim&amp;uuml;lasyonumuz"&lt;/strong&gt; var. Talep, &lt;strong&gt;constants.rs&lt;/strong&gt;'te tanımladığımız &lt;strong&gt;INVALID_MAIL&lt;/strong&gt; adresini i&amp;ccedil;eriyorsa sunucu bilerek &lt;strong&gt;421 Service Not Available&lt;/strong&gt; d&amp;ouml;n&amp;uuml;yor. Bu, istemci tarafındaki hata y&amp;ouml;netimi kodunu&lt;em&gt;(&amp;ouml;rneğin bir SmtpException yakalama bloğunu)&lt;/em&gt; ger&amp;ccedil;ek bir sunucuya ihtiya&amp;ccedil; duymadan test edebilmemi sağlıyor. Bağlantı &lt;strong&gt;QUIT&lt;/strong&gt; ile ya da karşı tarafın soketi kapatmasıyla&lt;em&gt;(okuma 0 bayt d&amp;ouml;n&amp;uuml;nce)&lt;/em&gt; sonlanıyor. &lt;strong&gt;ConnectionReset&lt;/strong&gt; gibi beklenmedik durumlar oluştuğundaysa loga bilgi d&amp;uuml;ş&amp;uuml;r&amp;uuml;l&amp;uuml;p d&amp;ouml;ng&amp;uuml;den &amp;ccedil;ıkılıyor.&lt;/p&gt;
&lt;p&gt;Her şeyi main fonksiyonunda topluyoruz. &lt;strong&gt;Tokio&lt;/strong&gt; &amp;ccedil;alışma zamanını kullandığımız i&amp;ccedil;in metodumuz hem asenkron hem de&lt;strong&gt; [tokio::main]&lt;/strong&gt; direktifi ile işaretlenmiş durumda.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use crate::constants::*;
use crate::server::Server;
use log::*;
use std::env;
use tokio::fs::create_dir_all;
use tokio::net::TcpListener;

mod constants;
mod server;
mod utility;

#[tokio::main]
async fn main() -&amp;gt; tokio::io::Result&amp;lt;()&amp;gt; {
    env::set_var("RUST_LOG", "tokio=info,light_mail_server=info");
    env_logger::init();

    create_dir_all(EMAIL_DIR).await?;

    let listener = TcpListener::bind(("0.0.0.0", 2525)).await?;
    info!("Light SMTP Server running on 0.0.0.0:2525");

    loop {
        let (mut socket, addr) = listener.accept().await?;
        info!("Connection received from {}", addr);

        tokio::spawn(async move {
            Server::handle(&amp;amp;mut socket)
                .await
                .expect("Failed to run server");
        });
    }
}&lt;/pre&gt;
&lt;p&gt;Uygulamanın giriş noktası olan main.rs fonksiyonu klasik bir tokio giriş noktası. &amp;Ouml;nce &lt;strong&gt;RUST_LOG&lt;/strong&gt; ortam değişkenini ayarlayıp &lt;strong&gt;env_logger&lt;/strong&gt; ortamını başlatıyoruz, ardından e-postaların yazılacağı &lt;strong&gt;EMAIL_DIR&lt;/strong&gt; klas&amp;ouml;r&amp;uuml;n&amp;uuml;n var olduğundan emin oluyoruz&lt;em&gt;(create_dir_all klas&amp;ouml;r&amp;uuml; zaten varsa hata vermiyor)&lt;/em&gt;. Sonrasında &lt;strong&gt;0.0.0.0:2525&lt;/strong&gt; &amp;uuml;zerinde bir &lt;strong&gt;TcpListener&lt;/strong&gt; a&amp;ccedil;ılıyo&lt;em&gt;r(yani sunucu t&amp;uuml;m ağ aray&amp;uuml;zlerinden 2525 portuna gelen bağlantıları dinlemeye başlıyor)&lt;/em&gt;. Sonsuz d&amp;ouml;ng&amp;uuml;de her yeni bağlantı kabul edildiğinde&lt;strong&gt; tokio::spawn&lt;/strong&gt; ile ayrı bir asenkron g&amp;ouml;rev&lt;em&gt;(task)&lt;/em&gt; başlatılıyor ve o bağlantının t&amp;uuml;m yaşam d&amp;ouml;ng&amp;uuml;s&amp;uuml; &lt;strong&gt;Server::handle&lt;/strong&gt; metoduna devrediliyor. B&amp;ouml;ylece birden fazla istemci aynı anda bağlanabiliyor ve her biri kendi g&amp;ouml;revinde, birbirinden bağımsız şekilde ele alınabiliyor.&lt;/p&gt;
&lt;h2&gt;İlk Testler&lt;/h2&gt;
&lt;p&gt;Dilerseniz uygulamayı bu noktada &amp;ccedil;alıştırıp en azından ayağa kalkıp kalkmadığına bakalım.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Eylul/LightMailSrv_00.png" alt="" /&gt;&lt;/p&gt;
&lt;h2&gt;Dockerize İşlemleri&lt;/h2&gt;
&lt;p&gt;&amp;Ouml;rneğimiz hazır ve işler durumda g&amp;ouml;r&amp;uuml;n&amp;uuml;yor. Bu &amp;ccedil;alışmayı yaptığımda &amp;ouml;rneği bir&lt;strong&gt; Docker Container&lt;/strong&gt; olarak ayağa kaldırıp kullanmıştım. &lt;strong&gt;Dockerize&lt;/strong&gt; işlemleri i&amp;ccedil;in bir imaj&lt;em&gt;(image)&lt;/em&gt; oluşturmamız gerekiyor. Bunun i&amp;ccedil;in uygulamanın &amp;ccedil;alışacağı ortamı ve gereksinimleri tanımlayan bir Dockerfile dosyasına ihtiyacımız var. Onu da aşağıdaki gibi yazabiliriz.&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;FROM rust:1.84.0 AS builder

WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y libssl-dev ca-certificates &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /app/target/release/light_mail_server /app/

CMD ["./light_mail_server"]&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;, iki aşamalı&lt;em&gt;(multi-stage)&lt;/em&gt; bir yapı kullanıyor. İlk aşamada &lt;strong&gt;rust:1.84.0&lt;/strong&gt; imajı &amp;uuml;zerinde &lt;strong&gt;cargo build --release&lt;/strong&gt; ile derleme yapılıyor. İkinci aşamada ise sadece derlenmiş &lt;strong&gt;binary&lt;/strong&gt;, &amp;ccedil;ok daha k&amp;uuml;&amp;ccedil;&amp;uuml;k olan &lt;strong&gt;debian:bookworm-slim&lt;/strong&gt; imajına kopyalanıyor. Bu sayede son imaj, &lt;strong&gt;Rust&lt;/strong&gt; derleyicisini ve ara derleme dosyalarını i&amp;ccedil;ermiyor. Sadece &amp;ccedil;alıştırılabilir dosyayı ve gerekli sertifikaları barındırıyor. Bir nevi imajı optimize edip &amp;ccedil;alışma zamanı maliyetini d&amp;uuml;ş&amp;uuml;rm&amp;uuml;ş oluyoruz diyebilirim. İmajı oluşturmak ve tek başına &amp;ccedil;alıştırmak i&amp;ccedil;in aşağıdaki komutları kullanabiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;# Docker imajını oluşturmak i&amp;ccedil;in
docker build -t light-mail-server .

# İmajı doğrudan &amp;ccedil;alıştırmak i&amp;ccedil;in
docker run -p 2525:2525 light-mail-server&lt;/pre&gt;
&lt;p&gt;Buna bağlı olarak işi kolaylaştırmak adına bir &lt;strong&gt;docker-file&lt;/strong&gt; dosyası i&amp;ccedil;erisine servis tanımını da ekleyebiliriz.&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;services:
  light-mail-server:
    build: .
    ports:
      - "2525:2525"&lt;/pre&gt;
&lt;h2&gt;O Zaman Bir İstemci(Client) Yazalım&lt;/h2&gt;
&lt;p&gt;Artık ortamımızda &lt;strong&gt;Docker&lt;/strong&gt; container olarak &amp;ccedil;alışan ve temel &lt;strong&gt;SMTP&lt;/strong&gt; hizmetini taklit eden bir e-posta sunucumuz var. Peki nasıl kullanacağız? İstemci tarafını&lt;strong&gt; .NET&lt;/strong&gt; tabanlı basit bir konsol uygulaması olarak tasarlayabiliriz. Aşağıdaki kod &amp;ouml;rneği gayet yeterli olacaktır.&lt;/p&gt;
&lt;pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false"&gt;using System.Net.Mail;

SendEmail("burak.selim@azon.com", "&amp;lt;h1&amp;gt;Challenge Accepted&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;This is a sample e-mail from dotnet client&amp;lt;/p&amp;gt;", true);
SendEmail("thereisnomail@nowhere.nowhere", "&amp;lt;h1&amp;gt;Error Test&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;We are waiting for 421 error.&amp;lt;/p&amp;gt;", true);

static void SendEmail(string to, string body, bool isHtml)
{
    try
    {
        var client = new SmtpClient("127.0.0.1", 2525)
        {
            DeliveryMethod = SmtpDeliveryMethod.Network
        };

        var message = new MailMessage("test.pilot@azon.com", to, "Demonstration Subject", body)
        {
            IsBodyHtml = isHtml
        };

        Console.WriteLine($"Sending email to {to}...");
        client.Send(message);
        Console.WriteLine("Email sent successfully!\n");
    }
    catch (SmtpException ex)
    {
        Console.WriteLine($"SMTP Error: {ex.StatusCode} - {ex.Message}\n");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"General Error: {ex.Message}\n");
    }
}&lt;/pre&gt;
&lt;p&gt;İstemci taraf, &lt;strong&gt;System.Net.Mail.SmtpClient&lt;/strong&gt; sınıfını kullanıyor ve &lt;strong&gt;127.0.0.1:2525&lt;/strong&gt; adresine, yani bizim &lt;strong&gt;light_mail_server&lt;/strong&gt;'ımıza bağlanacak şekilde ayarlanıyor. &lt;strong&gt;SendEmail&lt;/strong&gt; fonksiyonu bilerek iki farklı senaryoyu deniyor. İlk &amp;ccedil;ağrıda normal bir &lt;strong&gt;HTML&lt;/strong&gt; e-postası g&amp;ouml;nderiliyor ve sunucu bunu &lt;strong&gt;250 Message accepted for delivery&lt;/strong&gt; ile onaylıyor. İkinci &amp;ccedil;ağrıda ise alıcı olarak sunucu tarafında &amp;ouml;zel anlam taşıyan&lt;strong&gt; thereisnomail@nowhere.nowhere&lt;/strong&gt; adresi veriliyor. Bu adres, &lt;strong&gt;RCPT TO&lt;/strong&gt; aşamasında sunucuyu bilerek &lt;strong&gt;421 Service Not Available&lt;/strong&gt; d&amp;ouml;nmeye zorluyor ve istemci tarafındaki catch&lt;em&gt;(SmtpException ex)&lt;/em&gt; bloğunun&lt;em&gt;(yani test etmek isteyeceğimiz hata y&amp;ouml;netimi kodunun)&lt;/em&gt; ger&amp;ccedil;ekten &amp;ccedil;alıştığını, hakiki bir &lt;strong&gt;SMTP&lt;/strong&gt; sağlayıcısına ihtiya&amp;ccedil; duymadan doğrulamış oluyoruz.&lt;br /&gt;&lt;br /&gt;&amp;Ouml;yleyse taze taze bir deneme yapalım. &amp;Ouml;nce &lt;strong&gt;docker container&lt;/strong&gt;'ı başlatalım ve istemci uygulamayı &amp;ccedil;alıştıralım. Ben &lt;strong&gt;Windows&lt;/strong&gt; sistemimde aşağıdaki ekran g&amp;ouml;r&amp;uuml;nt&amp;uuml;s&amp;uuml;nde yer alan sonu&amp;ccedil;lara ulaştım.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Eylul/LightMailSrv_01.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;O zamanlar benim i&amp;ccedil;in keyifli bir &amp;ccedil;alışma olmuştu. Yaklaşık bir sene sonrasında ş&amp;ouml;yle bir geriye d&amp;ouml;n&amp;uuml;p bakınca yazı haline getirmenin iyi olacağına karar verdim. Umarım sizler i&amp;ccedil;in de faydalı bir pratik olur. B&amp;ouml;ylece geldik bir maceramızın daha sonuna. Tekrardan g&amp;ouml;r&amp;uuml;ş&amp;uuml;nceye dek hepinize mutlu g&amp;uuml;nler dilerim.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/buraksenyurt/friday-night-programmer/tree/main/src/light_mail_server" target="_blank"&gt;Github&lt;/a&gt; reposunda rust program kodlarına ulaşabilirsiniz.&lt;/p&gt;</summary>
    <published>2026-09-07T10:52:00+00:00</published>
    <link rel="related" href="https://www.buraksenyurt.com/post/testler-icin-bir-mail-sunucusu-yazmak#comment" />
    <category term="Rust" />
    <betag:tag>rust</betag:tag>
    <betag:tag>rust programming language</betag:tag>
    <betag:tag>programming</betag:tag>
    <betag:tag>docker</betag:tag>
    <betag:tag>dockerize</betag:tag>
    <betag:tag>container</betag:tag>
    <betag:tag>cSharp</betag:tag>
    <betag:tag>dotnet</betag:tag>
    <dc:publisher>bsenyurt</dc:publisher>
    <dc:description>Uzun zaman önce Friday Night Programmer laboratuvarında bir e-posta sunucusuna ihtiyacım oldu. Amacım belli bir sürecin entegrasyon testleri sırasında bir e-posta sunucusuna gidip gelme aksiyonunu taklit etmekti. Süreçte ihtiyacım olan entegrasyon testi, ortamda gerçek bir e-posta sunucusu olmadan da çalışabilmeliydi. Nitekim asıl odak noktam e-posta'ların gidip gitmemesi değildi. Sürecin bir parçası olarak ilgili e-posta'lar gitmiş ve sunucudan olumlu cevap almışım gibi hareket etmek istiyordum. Pekala bu konuda yazılmış çeşitli fake sunucular olabilirdi ama bana hafif sıklet bir aparat gerekiyordu. Aklıma rust ve bu tip sunucu bazlı işlemlerde öne çıkan tokio küfesi(crate) geldi.</dc:description>
    <pingback:server>https://www.buraksenyurt.com/pingback.axd</pingback:server>
    <pingback:target>https://www.buraksenyurt.com/post.aspx?id=279d0ee3-e679-496b-8873-7dd6f5445828</pingback:target>
    <slash:comments>0</slash:comments>
    <trackback:ping>https://www.buraksenyurt.com/trackback.axd?id=279d0ee3-e679-496b-8873-7dd6f5445828</trackback:ping>
    <wfw:comment>https://www.buraksenyurt.com/post/testler-icin-bir-mail-sunucusu-yazmak#comment</wfw:comment>
    <wfw:commentRss>https://www.buraksenyurt.com/syndication.axd?post=279d0ee3-e679-496b-8873-7dd6f5445828</wfw:commentRss>
  </entry>
  <entry>
    <id>https://www.buraksenyurt.com/post/c-ile-basit-bir-ecs-denemesi</id>
    <title>Kendi Bevy'ni Yazmaya Çalışmak: C# ile Basit Bir ECS Denemesi</title>
    <updated>2026-09-02T07:59:00+00:00</updated>
    <link rel="self" href="https://www.buraksenyurt.com/post.aspx?id=1aa0cbeb-12fa-4bdc-a937-158ac1f00da2" />
    <link href="https://www.buraksenyurt.com/post/c-ile-basit-bir-ecs-denemesi" />
    <author>
      <name>bsenyurt</name>
    </author>
    <summary type="html">&lt;p&gt;Bir s&amp;uuml;re Rust programlama dili ile oyun geliştirmeye &amp;ccedil;alışmıştım. Topluluğun g&amp;uuml;c&amp;uuml; olsa gerek &lt;a href="https://arewegameyet.rs/" target="_blank"&gt;Are We Game Yet&lt;/a&gt; sitesinde bir&amp;ccedil;ok framework, oyun motoru, render platformu vs var. En &amp;ccedil;ok kullanılan platformlardan birisi sanıyorum ki &lt;a href="https://crates.io/crates/bevy" target="_blank"&gt;Bevy&lt;/a&gt;. Onu &amp;ouml;zel kılan y&amp;ouml;nlerden birisi Composition over Inheritance prensibini merkeze alan Entity Component System implementasyonu da&amp;nbsp;sunması. Bir &amp;ouml;nceki c&amp;uuml;mlede iki &amp;ouml;nemli kavramdan bahsettim; kalıtım yerine kompozisyonun tercih edilmesi ve entity nesneleri ile donatılmış bileşenlerin ele alındığı sistemler yaklaşımı. Bu yazıdaki amacım Rust tarafında ECS kullanımını g&amp;ouml;stermekten ziyade s&amp;ouml;z konusu yaklaşımı C# gibi nesne y&amp;ouml;nelimli bir dilde tasarlamak istersek nasıl bir yol izleyebileceğimizi keşfetmek&lt;em&gt;(Bevy ile ilgili bir denememi merak ediyorsanız &lt;a href="https://github.com/buraksenyurt/game-dev-with-rust/blob/main/bevy/on-my-way" target="_blank"&gt;On My Way&lt;/a&gt; isimli oyunuma bakabilirsiniz)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Eylul/WriteOwnBevy_00.png" alt="" /&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Oyundan muhteşem bir enstantane :P&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Rust Tarafında Nasıldı?&lt;/h2&gt;
&lt;p&gt;Bevy'nin o zamanlar kullandığım s&amp;uuml;r&amp;uuml;m&amp;uuml;n&amp;uuml; baz alarak On My Way oyunundaki main fonksiyonunu kısaca değerlendirelim.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;mod events;
mod game;
mod main_menu;
mod systems;

use crate::events::GameOverEvent;
use crate::game::GamePlugin;
use crate::main_menu::MainMenuPlugin;
use crate::systems::*;
use bevy::prelude::*;

fn main() {
    App::new()
        .add_message::&amp;lt;GameOverEvent&amp;gt;()
        .add_plugins(DefaultPlugins)
        .init_state::&amp;lt;AppState&amp;gt;()
        .add_plugins((GamePlugin, MainMenuPlugin))
        .add_systems(Startup, spawn_camera)
        .add_systems(
            Update,
            (
                change_to_game_state,
                change_to_main_menu,
                handle_game_over,
                exit_game,
            ),
        )
        .run();
}

#[derive(States, Debug, Clone, Eq, PartialEq, Hash, Default)]
pub enum AppState {
    #[default]
    MainMenu,
    Game,
    GameOver,
}&lt;/pre&gt;
&lt;p&gt;Oyun d&amp;ouml;ng&amp;uuml;s&amp;uuml; aslında bir durum makinesi&lt;em&gt;(State Machine)&lt;/em&gt; gibi d&amp;uuml;ş&amp;uuml;n&amp;uuml;lebilir. Bu state'ler men&amp;uuml;de olma, oyunu oynama ve oyunda yanma hallerini ele alıyor. Bevy sağladığı zincir fonksiyonelliklerle bu makine i&amp;ccedil;in gerekli t&amp;uuml;m unsurları sağlıyor. &amp;Ouml;rneğin ekranla ilgili bilgiler veya kamera &amp;ouml;zellikleri dahili olarak sağladığı plug-in'ler &amp;uuml;zerinden geliyor. Kendi tasarladığımız plug-in'ler de ise oyundaki varlıkları&lt;em&gt;(entities)&lt;/em&gt;, bileşenleri&lt;em&gt;(components) &lt;/em&gt;ve işleyen sistemleri tarifleyebiliyoruz. T&amp;uuml;m bunlar durum makinesinin işletildiği sonsuz d&amp;ouml;ng&amp;uuml;de g&amp;uuml;ncellemeler sırasında&lt;em&gt;(Frame per second anlarında)&lt;/em&gt; veya başlangı&amp;ccedil; durumunda devreye giriyor. Burası kodun son derece yalınlaştığı bir sorumluluklar zinciri&lt;em&gt;(Chane of responsibility)&lt;/em&gt; implementasyonu. Konuyu biraz daha a&amp;ccedil;mak adına &amp;ouml;rnek bir plug-in kurgusunu masaya yatıralım. Oyuncunun kullandığı uzay gemisini ele alarak başlayalıım. Bu veri yapısı aslında sistem i&amp;ccedil;erisinde bir varlıkla ilişkilendirilecek bir bileşen&lt;em&gt;(component)&lt;/em&gt;.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use bevy::prelude::*;
#[derive(Component)]
pub struct Spaceship {}&lt;/pre&gt;
&lt;p&gt;Onun bir bileşen olduğunu Component direktifi ile Bevy oyun motoruna bildiriyoruz. Aslında bu veri yapısı &amp;ccedil;oğunlukla tek başına değil oyun motoru tarafında &amp;uuml;retilen bir varlıkla&lt;em&gt;(entity)&lt;/em&gt; ilişkilendirilerek kullanılıyor. Pek tabii bu tip bir veri modeli &amp;ccedil;eşitli &amp;ouml;zelliklere de sahip olabilir. &amp;Ouml;rneğin aşağıdaki roket veri yapısını g&amp;ouml;z &amp;ouml;n&amp;uuml;ne alalım.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use bevy::prelude::*;
#[derive(Component, Clone)]
pub struct Missile {
    pub direction: Vec2,
    pub speed: f32,
    pub width: f32,
    pub fuel_cost: f32,
    pub disposable: bool,
    pub location: Vec3,
}&lt;/pre&gt;
&lt;p&gt;Bir roketin y&amp;ouml;n&amp;uuml;, hızı, yakıt miktarı, konumu gibi bilgiler de lazım olur. Zira oyuncu bir tuşa basıp f&amp;uuml;zeyi fırlattığında aslında yeni ve benzersiz bir entity oluştururken başka bir sistemde bu entity'nin değerlerini ele alarak &amp;ccedil;eşitli g&amp;uuml;ncellemeler yapar. F&amp;uuml;ze, belirtilen y&amp;ouml;ne doğru belli bir hızda ama yakıtı t&amp;uuml;kenene kadar hareket eder. T&amp;uuml;m bu değerlendirmeler oyun motorunun sonsuz d&amp;ouml;ng&amp;uuml;s&amp;uuml;ndeki anlık g&amp;uuml;ncellemelerde değerlendirilir. System olarak ifade edilen fonksiyonlarsa bu g&amp;uuml;ncellemeleri &amp;uuml;stlenerek varlıkların verilerinde ya da uygulama durumlarında değişiklikler yaparlar. &amp;Ouml;rneğin bir f&amp;uuml;zenin hareketi sadece buna odaklanmış bir sistemle ele alınabilir. Aşağıdaki fonksiyon tam olarak bu g&amp;ouml;revi &amp;uuml;stleniyor.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;pub fn move_missile(mut query: Query&amp;lt;(&amp;amp;mut Transform, &amp;amp;Missile)&amp;gt;, time: Res&amp;lt;Time&amp;gt;) {
    for (mut transform, missile) in query.iter_mut() {
        let direction = Vec3::new(missile.direction.x, missile.direction.y, 0.);
        transform.translation += direction * missile.speed * time.delta_secs();
    }
}&lt;/pre&gt;
&lt;p&gt;Bu fonksiyonun parametre yapısı son derece &amp;ouml;nemli. Query generic bir t&amp;uuml;r ve aslında sahada değiştirilebilir Transform &amp;ouml;zelliği olan Missile referanslarını bekliyor. Sahada f&amp;uuml;zelerin olması oyuncunun tuşa basmasına bağlı ancak bu sistem tuşa basılma halini değil ortamda hareket halinde olması beklenen f&amp;uuml;ze varlıklarına ait referanslar var mı sorusunu cevaplıyor. Hatta sistemler arasındaki zamansal kaymaların &amp;ouml;n&amp;uuml;ne ge&amp;ccedil;mek i&amp;ccedil;in delta farklarını da bir Resource &amp;uuml;zerinden ele alıyor. Şimdi kafayı biraz karıştıralım. Oyuncu tuşa basıp f&amp;uuml;ze ateşleyecek. Aslında bu da bir sistem olarak ele alınmalı. Aşağıdaki fonksiyon bu işi &amp;uuml;stleniyor.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;pub fn fire_missile(
    mut commands: Commands,
    mut query: Query&amp;lt;&amp;amp;mut Transform, With&amp;lt;Spaceship&amp;gt;&amp;gt;,
    keyboard_input: Res&amp;lt;ButtonInput&amp;lt;KeyCode&amp;gt;&amp;gt;,
    asset_server: Res&amp;lt;AssetServer&amp;gt;,
    launch_timer: Res&amp;lt;MissileLaunchCheckTimer&amp;gt;,
    mut live_data: ResMut&amp;lt;LiveData&amp;gt;,
) {
    if keyboard_input.pressed(KeyCode::KeyS) &amp;amp;&amp;amp; launch_timer.timer.just_finished() {
        if let Ok(transform) = query.single_mut() {
            let missile = Missile {
                direction: Vec2::new(1., 0.),
                speed: MISSILE_003_SPEED,
                width: 51.,
                fuel_cost: 1.5,
                disposable: false,
                location: transform.translation,
            };
            commands.spawn((
                Sprite {
                    image: asset_server.load("sprites/spaceMissiles_003.png"),
                    ..default()
                },
                Transform::from_xyz(
                    transform.translation.x,
                    transform.translation.y,
                    0.,
                ),
                missile.clone(),
            ));
            live_data.spaceship_fuel_level -= missile.fuel_cost;
            live_data.used_missile_count += 1;
        }
    }
}&lt;/pre&gt;
&lt;p&gt;Kodun ilk etapta korkutucu geldiğini ifade edebilirim. Bu yazı a&amp;ccedil;ısından değerlendirirsek &amp;ouml;nemli olan noktaları vurgulamamız yeterli olacaktır. Sistemin ihtiya&amp;ccedil; duyacağı bir&amp;ccedil;ok malzeme parametre &amp;uuml;zerinden geliyor. Tuşa basılma durumunu kontrol edebileceğimiz, f&amp;uuml;ze resmini g&amp;ouml;sterebileceğimiz, f&amp;uuml;ze i&amp;ccedil;in saya&amp;ccedil; başlatabileceğimiz bazı kavramlar oyun i&amp;ccedil;in birer kaynak&lt;em&gt;(Resource)&lt;/em&gt;. Bir entity oluşturmamız gerektiğinden Commands bileşeni aktarılıyor. Elbette f&amp;uuml;zeyi ateşlemesi i&amp;ccedil;in ortamda birde Spaceship olması lazım. Ancak bu sistem&amp;nbsp;fonksiyonu i&amp;ccedil;in değiştirilebilir Transform bileşeni ile donatılmış bir Spaceship varlığına ihtiya&amp;ccedil; var, işte bunu da Query karşılıyor. Anlayacağınız &amp;ccedil;alışma zamanında sistemler &amp;uuml;zerinde &amp;ccedil;alışacakları varlıkları onları donattığımız bileşenler yardımıyla keşfedip kullanabilmekte ve ihtiya&amp;ccedil; duydukları kaynakları&lt;em&gt;(resim, ses, zamanlayıcı vs)&lt;/em&gt; yine genel ortamdan isteyebilmekte.&lt;/p&gt;
&lt;p&gt;Eğer kodları detaylıca incelerseniz sayısız sistem ve bileşen olduğunu g&amp;ouml;receksiniz. Bunları y&amp;ouml;netmek klasik bir kodlama ile &amp;ccedil;ok kolay değil. ECS&lt;em&gt;(Entity Component System)&lt;/em&gt; gibi prensiplerin uyarlanması oyun motorlarında bazı işleri olduk&amp;ccedil;a kolaylaştırıyor.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Pekala, elimizde entity haline d&amp;ouml;n&amp;uuml;şen veri yapıları, onları &amp;ccedil;eşitli karakteristiklerle donatabildiğimi bileşenler ve işlediğimiz sistemler var. Bunların sayısı bir noktadan sonra y&amp;ouml;netilmesi zor bir hale gelebiliyor. Aynı kapsama&lt;em&gt;(context)&lt;/em&gt; dahil olacak her şeyi paketlemek ve bir eklenti haline getirmek m&amp;uuml;mk&amp;uuml;n&lt;em&gt;(Bundle plug-in)&lt;/em&gt; &amp;Ouml;rneğin f&amp;uuml;zelerin kapsamını g&amp;ouml;z &amp;ouml;n&amp;uuml;ne alırsak ş&amp;ouml;yle bir plug-in kullanabiliyoruz.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use crate::game::missile::systems::*;
use crate::AppState;
use bevy::prelude::*;

pub mod components;
mod systems;
pub struct MissilePlugin;

impl Plugin for MissilePlugin {
    fn build(&amp;amp;self, app: &amp;amp;mut App) {
        app.add_systems(
            Update,
            (
                move_missile,
                detect_collision_with_meteors,
                claim_hitted,
                check_outside_of_the_bounds,
                animate_explosion_sprites,
                despawn_explosions,
            )
                .run_if(in_state(AppState::Game)),
        )
        .add_systems(OnExit(AppState::Game), despawn_missiles);
    }
}&lt;/pre&gt;
&lt;p&gt;Bevy bunun bir plugin olduğunu implement ettiği Plugin trait dolayısıyla biliyor. Bu trait parametre olarak uygulama nesnesini kullanıyor ve b&amp;ouml;ylece &amp;ccedil;alışma zamanına sistem ekleme kabiliyeti kazanılıyor. İşte ilk başta g&amp;ouml;sterdiğimiz main fonksiyonu i&amp;ccedil;erisindeki plug-in bildirimleri aslında kendi kapsamlarından bir&amp;ccedil;ok enstr&amp;uuml;manı &amp;ccedil;alışma zamanı enjekte edebiliyor.&lt;/p&gt;
&lt;h2&gt;ECS Hangi Avantajları Getiriyor?&lt;/h2&gt;
&lt;p&gt;İlk başta alışmakta zorlandığım bir programlama paradigmasıydı. &amp;Ouml;zellikle Bevy oyun motorunun da s&amp;uuml;rekli değişmesi ve tam bir stabil s&amp;uuml;r&amp;uuml;me gelmemesi işimi zorlaştırmıştı. Ancak &amp;ouml;nemli olan ECS'in getirdiği avantajları da anlamak. Bunları birka&amp;ccedil; başlık altında toplayabiliriz.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kompozisyonun kalıtıma tercih edilmesi&lt;em&gt;(Composition over Inheritance)&lt;/em&gt;:&lt;/strong&gt; Klasik nesne y&amp;ouml;nelimli bir oyun motorunda genelde GameObject temelli, derin bir kalıtım ağacı kurma eğiliminde oluruz. Meteor, Spaceship, Missile gibi tipler ortak bir taban sınıftan t&amp;uuml;rer, hareket edebilme, &amp;ccedil;izilebilme, hasar alabilme gibi yetenekler bu ağacın farklı katmanlarına dağılır. Zamanla elmas problemi&lt;em&gt;(diamond problem)&lt;/em&gt;&amp;nbsp;olarak da bilinen, bir sınıfın hem hareket edebilen hem menzili olan hem de &amp;ccedil;izilebilen bir şey olması gerektiğinde kalıtım ağacının nereye oturacağının belirsizleştiği a&amp;ccedil;maza d&amp;uuml;şebiliriz. ECS'de b&amp;ouml;yle bir kaygı yoktur. Nitekim bir varlık hangi davranışlara ihtiya&amp;ccedil; duyuyorsa o kadar bileşenle donatılır, hatta &amp;ccedil;alışma zamanında bile bir bileşen eklenip &amp;ccedil;ıkarılarak ilgili varlığın davranışı değiştirilebilir. S&amp;ouml;z gelimi bizim &amp;ouml;rneğimizdeki tower sadece Position ve Range bileşenlerini taşırken player hem Position hem Velocity bileşenleri ile donatılmıştır. Aralarında hi&amp;ccedil;bir kalıtım ilişkisi kurmamıza gerek kalmamıştır.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Veri&lt;em&gt;(data)&lt;/em&gt; ile davranışın&lt;em&gt;(behavior)&lt;/em&gt; birbirinden ayrılması:&lt;/strong&gt; Bileşenler saf veri, sistemler ise bu veri &amp;uuml;zerinde &amp;ccedil;alışan fonksiyonlar olarak d&amp;uuml;ş&amp;uuml;n&amp;uuml;lebilir. Bu ayrım test edilebilirliği artırır. Bu sayede bir sistemi test etmek i&amp;ccedil;in koca bir oyun d&amp;uuml;nyası kurmaya gerek de kalmaz. Duruma uygun birka&amp;ccedil; bileşen &amp;ouml;rneği hazırlayıp Apply metodunu &amp;ccedil;ağırmak yeterli olacaktır. Aynı zamanda "bu davranış hangi sınıfa ait olmalı" tartışması da ortadan kalkar. Davranış&lt;em&gt;(behavior)&lt;/em&gt; artık bir sınıfa değil, o davranışı &amp;uuml;stlenen sisteme ait hale gelir.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performans:&lt;/strong&gt; Ger&amp;ccedil;ek ECS motorları bileşenleri bellekte birbirine yakın kompakt dizilerde tutmakta&lt;em&gt;(Structure of Arrays yaklaşımı)&lt;/em&gt; Bir sistem sadece ilgilendiği bileşen tipini taradığında CPU cache dostu bir erişim deseni ortaya &amp;ccedil;ıkar. Bizim C# denememizde bunu hen&amp;uuml;z sağlamayacağız(Bir s&amp;uuml;re sonra...Sağlayamadı), List&amp;lt;IComponent&amp;gt; i&amp;ccedil;inde OfType&amp;lt;T&amp;gt;() ile filtreleme yapıyoruz ki bu hem tip kontrol&amp;uuml; hem de LINQ işlem maliyeti taşıyor ama ger&amp;ccedil;ek bir ECS motorunun neden bu kadar performans vaadinde bulunduğunu anlamak i&amp;ccedil;in bu noktayı bilmekte fayda var.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paralellik:&lt;/strong&gt; Sistemler hangi bileşenlere okuma/yazma erişimi istediklerini query &amp;uuml;zerinden bildirdiklerinde, birbiriyle &amp;ccedil;akışmayan sistemler aynı anda farklı thread'lerde &amp;ccedil;alıştırılabilir. Bevy'nin zamanlayıcısı&lt;em&gt;(scheduler)&lt;/em&gt; bunu otomatik olarak &amp;ccedil;&amp;ouml;zer. &amp;Ouml;rneğin move_missile ile hi&amp;ccedil;bir ortak bileşeni olmayan başka bir sistemi aynı frame i&amp;ccedil;inde paralel koşturabilir. Bizim tasarlayacağımız Scheduler sınıfı şu an i&amp;ccedil;in tamamen sıralı d&amp;uuml;zende &amp;ccedil;alışıyor ama mimarinin ileride bu t&amp;uuml;r bir genişlemeye kapalı olmadığını a&amp;ccedil;ık&amp;ccedil;a belirtmek isterim.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Gelelim C# Tarafına&lt;/h2&gt;
&lt;p&gt;Bevy'nin sağladığı ECS yetkinlikleri olduk&amp;ccedil;a &amp;ouml;nemli seviyede. Peki C# gibi bir nesne y&amp;ouml;nelimli dili g&amp;ouml;z &amp;ouml;n&amp;uuml;ne alırsak bu tip bir geliştirmeyi nasıl yaparız? En azından bir entity ve component ikilisi bir sistemde y&amp;uuml;r&amp;uuml;tebilmek ama bunu &amp;ccedil;alışma zamanındaki ana motor &amp;uuml;st&amp;uuml;nden sağlayabilmek ilk ama&amp;ccedil; olabilir. Yazının devam eden kısmında işte bu maceraya dalıyor olacağız. İşe temiz bir Console uygulaması ile başlamakta yarar var. Engine olarak isimlendirdiğim dosya i&amp;ccedil;eriğini aşağıdaki gibi doldurdum.&lt;/p&gt;
&lt;pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false"&gt;namespace NetBevy;

public interface IComponent
{
}

public interface IEntity
{
    Guid ID { get; set; }
    List&amp;lt;IComponent&amp;gt; Components { get; set; }
    void AddComponent(IComponent component);
}

public class Entity
    : IEntity
{
    public Guid ID { get; set; }
    public List&amp;lt;IComponent&amp;gt; Components { get; set; } = [];

    public void AddComponent(IComponent component)
    {
        Components.Add(component);
    }
}

public interface ISystem&amp;lt;T&amp;gt; where T : IComponent
{
    void Apply(IEnumerable&amp;lt;(Entity entity, T component)&amp;gt; components);
}

public enum SystemState
{
    Startup,
    Update
}

public class Scheduler
{
    private readonly World _world;

    private Dictionary&amp;lt;SystemState, List&amp;lt;object&amp;gt;&amp;gt; _systems = new()
    {
        { SystemState.Startup, new List&amp;lt;object&amp;gt;() },
        { SystemState.Update, new List&amp;lt;object&amp;gt;() }
    };

    public Scheduler(World world) =&amp;gt; _world = world;

    public void AddSystem&amp;lt;T&amp;gt;(SystemState state, ISystem&amp;lt;T&amp;gt; system) where T : IComponent
    {
        _systems[state].Add(system);
    }

    public void Run(SystemState state)
    {
        if (!_systems.TryGetValue(state, out List&amp;lt;object&amp;gt;? value)) return;

        foreach (var system in value)
        {
            var systemType = system
                .GetType()
                .GetInterfaces()
                .FirstOrDefault(i =&amp;gt; i.IsGenericType &amp;amp;&amp;amp; i.GetGenericTypeDefinition() == typeof(ISystem&amp;lt;&amp;gt;));

            if (systemType != null)
            {
                var componentType = systemType.GetGenericArguments()[0];
                var queryType = typeof(Query&amp;lt;&amp;gt;).MakeGenericType(componentType);
                var queryInstance = Activator.CreateInstance(queryType, _world);
                var getEntitiesMethod = queryType.GetMethod("GetEntities");
                var entities = getEntitiesMethod.Invoke(queryInstance, null);

                var applyMethod = systemType.GetMethod("Apply");
                _ = applyMethod.Invoke(system, [entities]);
            }
        }
    }
}


public class World
{
    private List&amp;lt;Entity&amp;gt; _entities = [];

    public Entity CreateEntity()
    {
        var entity = new Entity { ID = Guid.NewGuid() };
        _entities.Add(entity);
        return entity;
    }

    public IEnumerable&amp;lt;Entity&amp;gt; GetEntities() =&amp;gt; _entities;
}

public class Query&amp;lt;T&amp;gt; where T : IComponent
{
    private readonly World _world;

    public Query(World world) =&amp;gt; _world = world;

    public IEnumerable&amp;lt;(Entity entity, T component)&amp;gt; GetEntities()
    {
        return _world.GetEntities()
            .SelectMany(e =&amp;gt; e.Components
                .OfType&amp;lt;T&amp;gt;()
                .Select(c =&amp;gt; (e, c)));
    }
}&lt;/pre&gt;
&lt;p&gt;Şimdi burada neler olduğuna birlikte bakalım. Ne yapmak istediğimize dikkat etmemiz lazım. Engine i&amp;ccedil;erisindeki enstr&amp;uuml;manlar asıl oyun programcısının ekleyeceği ve motorun hi&amp;ccedil;bir suretle bilemeyeceği ama ele alması gereken kavramları sağlamalı. Bu nedenle aray&amp;uuml;zler&lt;em&gt;(interface)&lt;/em&gt; bizim i&amp;ccedil;in &amp;ouml;nemli akt&amp;ouml;rlerden birisi. Bileşenleri tariflemek i&amp;ccedil;in IComponent aray&amp;uuml;z&amp;uuml;n&amp;uuml; kullanıyoruz. Şu an i&amp;ccedil;in bir bileşenin oyun programıcısı tarafından mutlaka ele alması gereken davranışları&lt;em&gt;(behavior)&lt;/em&gt; yok.&lt;/p&gt;
&lt;p&gt;Entity'ler aslında programcının motordan istediği ve &amp;uuml;retilen(spawn) şeyler. Benzersiz bir kimlikleri olması ve ayrıca bileşenler ile donatılabilmeleri gerekiyor. Bir varlığın bileşenlerle donatılması aslında bize ş&amp;ouml;yle bir imkan sağlıyor; oyun başlar ve karakter sahaya gelir(spawn) ve gelirken zırhlanır, ateş g&amp;uuml;c&amp;uuml; kazanır, hareket eder vs. Zırhlanabilir, ateş edebilir&lt;em&gt;(Weapon)&lt;/em&gt;, hareket edebilir&lt;em&gt;(Moveable)&lt;/em&gt; aslında programcının tanımladığı ve belli nitelikler taşıyan bileşenler olarak d&amp;uuml;ş&amp;uuml;n&amp;uuml;lebilir. O halde sistemin bir varlığın nasıl tanımlanıp neler yapabileceğinin de tariflenmesi gerekiyor. Bu nedenle IEntity aray&amp;uuml;z&amp;uuml; i&amp;ccedil;erisinde Guid t&amp;uuml;r&amp;uuml;nden bir &amp;ouml;zellik(d&amp;uuml;ş&amp;uuml;nd&amp;uuml;m de şimdi sadece readonly de yapılabilirmiş), bileşen eklemeyi sağlayan bir metot&lt;em&gt;(AddComponent)&lt;/em&gt; ve sahip olduğu bileşenleri&amp;nbsp;&amp;ccedil;ekmemizi sağlayan başka bir &amp;ouml;zellik yer alıyor.&lt;/p&gt;
&lt;p&gt;Varlıkların ve bileşenlerin oyun motoru i&amp;ccedil;in gerekli olan s&amp;ouml;zleşmeleri&lt;em&gt;(contracts)&lt;/em&gt; hazır. Bunların değerlendirileceği yer ise sistemler. Ve sistemler yine oyun programcısı tarafından tanımlanıyor. Oyun motoru oyunun kullandığı sistemler hakkında bilgi sahibi olamaz ama hangi kurallara uymaları gerektiğini s&amp;ouml;zleşemeler aracılığıyla belirtebilir. Bunu ISystem aray&amp;uuml;z&amp;uuml; ile sağlıyoruz. Dikkat edileceği &amp;uuml;zere sistem s&amp;ouml;zleşmesi generic ve T t&amp;uuml;r&amp;uuml;n&amp;uuml;n IComponent implementasyonu yapmış olmasını bekliyor. Tek bir davranışı var o da Apply. Metodun parametre yapısı &amp;uuml;zerinden hareket edersek Entity ve IComponent tuple'larından oluşan numaralanabilir bir liste aldığını g&amp;ouml;rebiliriz. Tabii bu &amp;ccedil;ok ilkel bir sistem fonksiyonelliği. Rust gibi motorların sağladığı varyasyonlar d&amp;uuml;ş&amp;uuml;n&amp;uuml;ld&amp;uuml;ğ&amp;uuml;nde inanılmaz derecede yalın kalıyor. Ancak hedefe giden yolda işimize yarayacağını a&amp;ccedil;ık&amp;ccedil;a s&amp;ouml;yleyebilirim.&lt;/p&gt;
&lt;p&gt;Bevy tarafındaki sistem fonksiyonlarını hatırlarsanız eğer d&amp;uuml;nyaya h&amp;uuml;kmetmek i&amp;ccedil;in Command ve d&amp;uuml;nyadaki şeyleri &amp;ccedil;etrefilli bi&amp;ccedil;imde sorgulayabilmek i&amp;ccedil;in akıllı bir Query bileşenine sahipti. Burada benzer bir işlevselliği sağlamak i&amp;ccedil;in program kodun alt kısımlarında g&amp;ouml;receğiniz World ve generic Query sınıflarını kullanıyoruz. World sınıfı aslında oyun motorunun sağladığı d&amp;uuml;nyadaki enstr&amp;uuml;manları sunuyor diyebiliriz. Şu an i&amp;ccedil;in sadece Entity'leri y&amp;ouml;netiyoruz. Entity oluşturmak ve hafızaya almak var olan entity listesini &amp;ccedil;ekmek&amp;nbsp;sağladığı fonksiyonellikler. Generic Query sınıfı ise bir World nesne &amp;ouml;rneği ile var olabilen bir bileşen. Sınıfın belki de en &amp;ouml;nemli noktası GetEntities i&amp;ccedil;erisindeki LINQ&lt;em&gt;(Language INtegrated Query)&lt;/em&gt; sorgusu.&amp;nbsp;Entity'leri alırken s&amp;ouml;z konusu bileşene sahip olanları &amp;ccedil;ekiyor. Tabii şu an i&amp;ccedil;in tek seviyede bir sorgulama m&amp;uuml;mk&amp;uuml;n. &amp;Ouml;rneğin, hareket edebilen varlıklar i&amp;ccedil;in bir hareket sistemi yazmamız kolay lakin hareket edebilen ve ateş g&amp;uuml;c&amp;uuml; olan varlıkları &amp;ccedil;ekmek şimdilik m&amp;uuml;mk&amp;uuml;n değil. Bu da size bir &amp;ouml;dev olsun. Sorgunun tek component yerine n adet component'i sorguya alması yeterli olacaktır.&lt;/p&gt;
&lt;p&gt;Şu ana kadar oyun motoru i&amp;ccedil;in gerekli bir&amp;ccedil;ok unsuru tesis ettik. Varlık oluşturmak, bileşen tanımlayıp varlıklarla ilişkilendirmek, bileşenleri baz alan sistemler tasarlamak ve hatta sorgulamak. Ancak oyun motorunun programcısına sunması gereken durum y&amp;ouml;netimini yapan planlayıcı&lt;em&gt;(Scheduler)&lt;/em&gt; bileşeni de gerekiyor. Scheduler sınıfının tam olarak yaptığı şey de bu. Bir World nesnesi ile başlatılabiliyor. Oyun d&amp;uuml;nyasının işleteceği sistemleri state bazlı olarak bir Dictionary'de tutuyor. Scheduler sayesinde programcı tasarladığı sistemleri dahili gelen oyun d&amp;uuml;nyasına ekleyebiliyor. Run metodunun i&amp;ccedil;eriği ise kayda değer. Temel olarak sistemleri bulup Apply metotlarını işlettiğini ifade edebiliriz. Tabii pek &amp;ccedil;ok handikapı var. Type &amp;uuml;st&amp;uuml;nden reflection ile gittiğinde sistemleri y&amp;uuml;kleme ve fonksiyon &amp;ccedil;ağırma aksiyonlarının sahne başına yapılması ciddi bir y&amp;uuml;k. Ancak bunu optimize etmek sonraya bıraktığım ve kabullendiğim bir teknik bor&amp;ccedil;. Hen&amp;uuml;z ilk denemeyi yapıyoruz. &amp;Ccedil;alışır bir versiyon elde etmek kritik.&lt;/p&gt;
&lt;h2&gt;O Zaman Deneyelim&lt;/h2&gt;
&lt;p&gt;Aslında oyun motoru kabaca hazır. Denemek i&amp;ccedil;in oyun programcısı rol&amp;uuml;ne b&amp;uuml;r&amp;uuml;nelim. İlk olarak ihtiyacımız olan bileşenleri&lt;em&gt;(components)&lt;/em&gt; tasarlayalım.&lt;/p&gt;
&lt;pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false"&gt;namespace NetBevy.Game;

public class Position
    : IComponent
{
    public float X { get; set; }
    public float Y { get; set; }
}

public class Velocity
    : IComponent
{
    public float X { get; set; }
    public float Y { get; set; }
}

public class Range
    : IComponent
{
    public float Value { get; set; }

}&lt;/pre&gt;
&lt;p&gt;Klasik bileşenlerimiz var. Pozisyon, ivme ve mesafe gibi. Bunlar ger&amp;ccedil;ektende oyundaki bir varlıkla ilişkilendirildiklerinde daha &amp;ccedil;ok anlam kazanıyorlar. Hatta o zaman Query'ler de mantıklı hale geliyor. Hareket edebilen ve mesafe kat edebilen şeyler esasında bu oyun programıcısı a&amp;ccedil;ısından Velocity, Range ve Position bileşenleri ile donatılmış varlıklar. Bir kale sadece Position ve Range bileşeni i&amp;ccedil;erirken şehrin giriş kapısı sadece position bileşenine sahiptir. &amp;Ouml;yleyse iki de &amp;ouml;rnek sistem ekleyelim.&lt;/p&gt;
&lt;pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false"&gt;namespace NetBevy.Game;

public class SetupPositionSystem : ISystem&amp;lt;Position&amp;gt;
{
    public void Apply(IEnumerable&amp;lt;(Entity entity, Position component)&amp;gt; components)
    {
        foreach (var (entity, position) in components)
        {
            position.X = 0;
            position.Y = 0;
            Console.WriteLine($"[Setup] Entity {entity.ID} initialized at (0,0)");
        }
    }
}

public class MovementSystem : ISystem&amp;lt;Position&amp;gt;
{
    public void Apply(IEnumerable&amp;lt;(Entity entity, Position component)&amp;gt; components)
    {
        foreach (var (entity, position) in components)
        {
            position.X += 1.0f;
            position.Y += 1.0f;
            Console.WriteLine($"[Update] Entity {entity.ID} moved to ({position.X}, {position.Y})");
        }
    }
}&lt;/pre&gt;
&lt;p&gt;İlk sistem Position bileşeni olan ne kadar varlık varsa onların konumlarını sıfırlıyor. Oyun i&amp;ccedil;in anlamsız ama bizim testlerimiz i&amp;ccedil;in kafi. MovementSystem isimli sınıf ise Position bileşeni y&amp;uuml;kl&amp;uuml; olan t&amp;uuml;m varlıkların X ve Y değerlerini değiştiriyor. Evet &amp;ccedil;ok anlamsız ancak bir &amp;ouml;nceki c&amp;uuml;mlede de belirttiğim gibi bunlar bizim sistem testlerimiz. Dikkat edeceğimiz noktalardan birisi her iki sistemin de ISystem aray&amp;uuml;z&amp;uuml;n&amp;uuml; implemente etmesi. Dolayısıyla oyun motorunun ihtiya&amp;ccedil; duyacağı Apply davranışını uygulamak zorundalar. Oyun programcısı olarak koda baktığımızda bazı şeyler biraz daha net g&amp;ouml;r&amp;uuml;n&amp;uuml;yor. O y&amp;uuml;zden artık oyunun kendisini geliştirmek de kolay. Aşağıdaki sınıfı g&amp;ouml;z &amp;ouml;n&amp;uuml;ne alalım.&lt;/p&gt;
&lt;pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false"&gt;namespace NetBevy.Game;

public static class GameApp
{
    public static void Run()
    {
        World world = new();

        var player = world.CreateEntity();
        player.AddComponent(new Position { X = 10, Y = 10 });
        player.AddComponent(new Velocity { X = 1, Y = 0 });

        var enemy = world.CreateEntity();
        enemy.AddComponent(new Position { X = 100, Y = 10 });
        enemy.AddComponent(new Velocity { X = -1, Y = 0 });

        var tower = world.CreateEntity();
        tower.AddComponent(new Position { X = 0, Y = 0 });
        tower.AddComponent(new Range { Value = 85 });

        var scheduler = new Scheduler(world);

        scheduler.AddSystem(SystemState.Startup, new SetupPositionSystem());
        scheduler.AddSystem(SystemState.Update, new MovementSystem());

        scheduler.Run(SystemState.Startup);
        for (int i = 0; i &amp;lt; 3; i++)
        {
            scheduler.Run(SystemState.Update);
        }
    }
}&lt;/pre&gt;
&lt;p&gt;D&amp;uuml;nyada &amp;uuml;&amp;ccedil; varlık oluşturup planlayıcıyı kuruyor ve sistemleri ekliyoruz. Startup durumunu bir kez, Update durumunu ise &amp;uuml;&amp;ccedil; kez &amp;ccedil;alıştırarak varlıkların ardışık adımlarda nasıl g&amp;uuml;ncellendiğini g&amp;ouml;zlemliyoruz. Ana program kodumuz ise epeyce sade. Bir nevi u&amp;ccedil;tan uca test fonksiyonumuz diyebilirim.&lt;/p&gt;
&lt;pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false"&gt;using NetBevy.Game;

namespace NetBevy;

internal class Program
{
    static void Main()
    {
        // Basic Sample
        GameApp.Run();
    }
}&lt;/pre&gt;
&lt;p&gt;İşte &amp;ccedil;alışma zamanı &amp;ccedil;ıktımız.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Eylul/WriteOwnBevy_02.png" alt="" /&gt;&lt;/p&gt;
&lt;h2&gt;Daha Neler Yapılabilir?&lt;/h2&gt;
&lt;p&gt;Aslında planladığımız gibi&lt;em&gt;(en azından benim kafamda)&lt;/em&gt; y&amp;uuml;r&amp;uuml;yen iskelet hazır. Bir k&amp;uuml;t&amp;uuml;phane haline getirildiğinde bileşen eklenebilen, sistem tanımlanıp işletilebilen bir oyun motoruna sahibiz fakat elbette &amp;ccedil;ok fazla eksiği ve hatta iyileştirilmesi gereken yerler var. &amp;Ouml;rneğin;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Query sınıfını geliştirmek gerekiyor. Birden fazla IComponent t&amp;uuml;revini ele alabilmeli.&lt;em&gt;(G&amp;uuml;ncel geliştirmeler i&amp;ccedil;in &lt;a href="https://github.com/buraksenyurt/friday-night-programmer/tree/main/src/BevyDotNet" target="_blank"&gt;BevyDotNet &amp;ouml;rnek kodlarına&lt;/a&gt; bakabilirsiniz. Source Code Generator m&amp;uuml;dahalesi ile Query sınıf hi&amp;ccedil; olmazsa 8 bileşene kadar destek verebiliyor)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Sistem davranışlarında oyun d&amp;uuml;nyasında basit aksiyonları yapabilecek Command bileşen entegrasyonu gerekli. &amp;Ouml;rneğin bir sistem i&amp;ccedil;erisinde bir varlık oluşturmak World nesnesindeki Entity zincirine bir nesne &amp;ouml;rneği eklemek demek. Bu kabiliyet kazandırılabilir.&lt;/li&gt;
&lt;li&gt;Oyun motorları varlıklarla ilişkilendirilecek asset'ler i&amp;ccedil;in bir y&amp;ouml;netici mekanizma sunuyor. Zira ses, animasyon akışı, resim gibi bir&amp;ccedil;ok asset s&amp;ouml;z konusu. Bunların bellek dostu olacak şekilde y&amp;ouml;netilmesi başlı başına bir mevzu. &amp;Ouml;rneğin On My Way &amp;ouml;rneğimizdeki spawn_spaceship metodu Resource olarak bir AssetServer alıyor ve onun load metodu ile yeni bir varlık tanımlanırken ilişkilendiriliyor. B&amp;ouml;ylece draw mekanizması uzay gemisi varlığı i&amp;ccedil;in sahneye ilgili asset'i &amp;ccedil;izeceğini de biliyor. B&amp;uuml;y&amp;uuml;k &amp;ccedil;aplı bir oyunda load metodunun gecikmesiz &amp;ccedil;alışması, gereksiz yere y&amp;uuml;kleme yapmaması, zaten var ama ihtiya&amp;ccedil; yoksa alandan atması gibi d&amp;uuml;ş&amp;uuml;n&amp;uuml;lmesi gereken bir&amp;ccedil;ok şey var.&lt;/li&gt;
&lt;li&gt;Şu anki s&amp;uuml;r&amp;uuml;m sadece terminalden &amp;ccedil;alışan bir motor vaat ediyor. Ekran &amp;ccedil;izdirebilmeli, kamerayı 2D veya 3D olma durumuna g&amp;ouml;re ayarlayabilmeli ve bunları cross-platform sağlarken varsayılan versiyonlarını da programcıya sağlayabilmeli.&lt;/li&gt;
&lt;li&gt;Sistemlerin &amp;ccedil;alışma sırası şu an tamamen eklenme sırasına bağlı. before/after gibi bağımlılık bildirimleriyle deterministik bir sıralama garanti altına alınabilir.&lt;/li&gt;
&lt;li&gt;Varlık ve bileşen &amp;ccedil;ıkarma&lt;em&gt;(despawn, RemoveComponent)&lt;/em&gt; yeteneği yok. Şu an motor sadece varlık ekleme işlemlerini yapabiliyor. Bir f&amp;uuml;ze menzil dışına &amp;ccedil;ıktığında ya da bir meteor vurulduğunda onu d&amp;uuml;nyadan silme gerekiyor. Bunu yabana atmayın. F&amp;uuml;zeler oluştuk&amp;ccedil;a belleği bir anda şişirebilirler ve oyun kitlenebilir&lt;em&gt;(A&amp;ccedil;ık&amp;ccedil;ası .Net gibi bir ortam yerine bellek g&amp;uuml;venliği konusunda &amp;ccedil;ok daha titiz olan Rust bu a&amp;ccedil;ıdan her zaman &amp;ouml;nde bana kalırsa)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Scheduler.Run i&amp;ccedil;erisindeki reflection tabanlı &amp;ccedil;&amp;ouml;z&amp;uuml;mleme her &amp;ccedil;alıştırmada tekrar tekrar ve tekrar yapılıyor :) Bu maliyet bir Source Generator ya da derleme zamanı kod &amp;uuml;retimiyle &amp;ouml;nemli &amp;ouml;l&amp;ccedil;&amp;uuml;de azaltılabilir esasında. İşte bakılacak bir konu daha. Source Generator.&lt;/li&gt;
&lt;li&gt;Sistemler arası iletişim i&amp;ccedil;in Bevy'deki Message/Event kavramına benzer bir yapı eklenebilir. Oyun başladı veya bitti gibi durumları bildirmek i&amp;ccedil;in b&amp;ouml;yle bir mekanizma epeyce faydalı olur.&lt;/li&gt;
&lt;li&gt;ve daha bir&amp;ccedil;ok şey...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bu liste elbette uzatılabilir. Asıl mesele k&amp;uuml;&amp;ccedil;&amp;uuml;k ama &amp;ccedil;alışan bir &amp;ccedil;ekirdekten yola &amp;ccedil;ıkıp &amp;uuml;zerine katman katman eklemek.&lt;/p&gt;
&lt;h2&gt;Sonu&amp;ccedil;&lt;/h2&gt;
&lt;p&gt;G&amp;ouml;r&amp;uuml;ld&amp;uuml;ğ&amp;uuml; &amp;uuml;zere, birka&amp;ccedil; aray&amp;uuml;z ve reflection tabanlı bir Scheduler ile bile temel bir ECS mimarisinin iskeletini C# tarafında ayağa kaldırmak m&amp;uuml;mk&amp;uuml;n. Elbette bu Bevy'nin sunduğu zenginliğin &amp;ccedil;ok gerisinde. Ne paralel &amp;ccedil;alışan sistemler var, ne asset y&amp;ouml;netimi ne de deklaratif bir plugin sistemi. Ama ama&amp;ccedil; zaten bunları birebir yeniden inşa etmek değildi. Kompozisyon, veri ile davranışın ayrılması ve sorgu&lt;em&gt;(query)&lt;/em&gt; tabanlı sistemlerin nesne y&amp;ouml;nelimli bir dilde nasıl karşılık bulabileceğini elle deneyerek g&amp;ouml;rmekti.&lt;/p&gt;
&lt;p&gt;Bir sonraki adımda yapmayı uzun s&amp;uuml;redir ertelediğim birka&amp;ccedil; şey var. Query sınıfını &amp;ccedil;oklu bileşen destekleyecek şekilde genişletmek, Command deseni ile sistemler i&amp;ccedil;inde g&amp;uuml;venli bi&amp;ccedil;imde varlık oluşturmak, silmek ve reflection y&amp;uuml;k&amp;uuml;n&amp;uuml; bir Source Generator ile derleme zamanına taşımak. Belki bunları da ayrı bir yazıda ele alırım. B&amp;ouml;ylece geldik bir &amp;ccedil;alışmamızın daha sonuna. Tekrardan g&amp;ouml;r&amp;uuml;ş&amp;uuml;nceye dek hepinize mutlu g&amp;uuml;nler dilerim.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kod &amp;ouml;rneklerinin ilk haline&amp;nbsp;&lt;a href="https://github.com/buraksenyurt/friday-night-programmer/tree/main/src/NetBevy" target="_blank"&gt;github&lt;/a&gt; reposundan ulaşabilirsiniz&lt;/li&gt;
&lt;li&gt;Ancak devam eden ser&amp;uuml;vene dahil olmak isterseniz &lt;em&gt;(&amp;Ouml;rneğin Source Code Generator eklenmiş bir versiyonu g&amp;ouml;rmek)&lt;/em&gt; &lt;a href="https://github.com/buraksenyurt/friday-night-programmer/tree/main/src/BevyDotNet" target="_blank"&gt;buradaki &amp;ouml;rneğe de&lt;/a&gt; bakabilirsiniz.&lt;/li&gt;
&lt;/ul&gt;</summary>
    <published>2026-09-02T07:59:00+00:00</published>
    <link rel="related" href="https://www.buraksenyurt.com/post/c-ile-basit-bir-ecs-denemesi#comment" />
    <category term="C#" />
    <betag:tag>game programming</betag:tag>
    <betag:tag>ecs</betag:tag>
    <betag:tag>entity component system</betag:tag>
    <betag:tag>cSharp</betag:tag>
    <betag:tag>rust</betag:tag>
    <betag:tag>bevy</betag:tag>
    <betag:tag>compositionOverInheritance</betag:tag>
    <betag:tag>oyun programlama</betag:tag>
    <dc:publisher>bsenyurt</dc:publisher>
    <dc:description>Bir süre Rust programlama dili ile oyun geliştirmeye çalışmıştım. Topluluğun gücü olsa gerek Are We Game Yet sitesinde birçok framework, oyun motoru, render platformu vs var. En çok kullanılan platformlardan birisi sanıyorum ki Bevy. Onu özel kılan yönlerden birisi Composition over Inheritance prensibini merkeze alan Entity Component System implementasyonu da sunması. Bir önceki cümlede iki önemli kavramdan bahsettim; kalıtım yerine kompozisyonun tercih edilmesi ve entity nesneleri ile donatılmış bileşenlerin ele alındığı sistemler yaklaşımı. Bu yazıdaki amacım Rust tarafında ECS kullanımını göstermekten ziyade ECS yaklaşımını C# gibi nesne yönelimli bir dilde tasarlamak istersek nasıl bir yol izleyebileceğimizi keşfetmek.</dc:description>
    <pingback:server>https://www.buraksenyurt.com/pingback.axd</pingback:server>
    <pingback:target>https://www.buraksenyurt.com/post.aspx?id=1aa0cbeb-12fa-4bdc-a937-158ac1f00da2</pingback:target>
    <slash:comments>0</slash:comments>
    <trackback:ping>https://www.buraksenyurt.com/trackback.axd?id=1aa0cbeb-12fa-4bdc-a937-158ac1f00da2</trackback:ping>
    <wfw:comment>https://www.buraksenyurt.com/post/c-ile-basit-bir-ecs-denemesi#comment</wfw:comment>
    <wfw:commentRss>https://www.buraksenyurt.com/syndication.axd?post=1aa0cbeb-12fa-4bdc-a937-158ac1f00da2</wfw:commentRss>
  </entry>
  <entry>
    <id>https://www.buraksenyurt.com/post/rust-ile-basit-bir-key-value-store-uygulamasi</id>
    <title>Rust ile Basit Bir Key-Value Store Uygulaması</title>
    <updated>2026-09-01T18:54:00+00:00</updated>
    <link rel="self" href="https://www.buraksenyurt.com/post.aspx?id=9fbc6222-c3ec-40fc-9f9a-13ef9c1eff40" />
    <link href="https://www.buraksenyurt.com/post/rust-ile-basit-bir-key-value-store-uygulamasi" />
    <author>
      <name>bsenyurt</name>
    </author>
    <summary type="html">&lt;p&gt;Bir programlama dilini &amp;ouml;ğrenmeye &amp;ccedil;alışırken bazı kavramları anlamanın en iyi yolu elbette senaryo bazlı pratik yapmaktan ge&amp;ccedil;iyor. Yazıyı yazdığım tarih itibariyle ş&amp;ouml;yle bir bakıyorum da repolarımın kıyısında k&amp;ouml;şesinde bu ama&amp;ccedil;la yazılmış ama unutulmuş denemeler var. Bug&amp;uuml;n onlardan birisini yazıya d&amp;ouml;k&amp;uuml;p tekrar etmeye &amp;ccedil;alışacağım. Elimdeki konu olduk&amp;ccedil;a sağlam. &amp;Ccedil;ok sık karşımıza &amp;ccedil;ıkan ama &amp;uuml;zerine fazla kafa yormadan ilerlediğimizde bize dert &amp;ccedil;ıkarabilecek &amp;uuml;&amp;ccedil; &amp;ouml;nemli par&amp;ccedil;ası var; &lt;strong&gt;eşzamanlılık&lt;em&gt;(concurrency)&lt;/em&gt;&lt;/strong&gt;, &lt;strong&gt;m&amp;uuml;şterek kilitleme&lt;em&gt;(mutex)&lt;/em&gt; &lt;/strong&gt;ve &amp;ccedil;ok kullanıcı tarafından ortaklaşa paylaşılmak istenen veri&lt;strong&gt;&lt;em&gt;(shared data)&lt;/em&gt;&lt;/strong&gt;. Bunları teorik olarak bilmek başka, aynı verinin birden fazla bağlantı tarafından aynı anda değiştirilmeye &amp;ccedil;alışıldığı bir senaryoda ele alıp test etmek bambaşka. O y&amp;uuml;zden basit ama ger&amp;ccedil;ek&amp;ccedil;i bir problem se&amp;ccedil;tim; &lt;strong&gt;TCP&lt;/strong&gt; protokol&amp;uuml; &amp;uuml;zerinden konuşan, verileri bellekte tutan, minik bir &lt;strong&gt;key-value store&lt;/strong&gt; yazmak.&lt;br /&gt;&lt;br /&gt;Ortaya &lt;strong&gt;kivi-store&lt;/strong&gt; adında k&amp;uuml;&amp;ccedil;&amp;uuml;k bir &lt;strong&gt;Rust&lt;/strong&gt; projesi &amp;ccedil;ıktı. Neden Kivi ismini kullandığımı &amp;ccedil;ok irdelemeyin. Meyve olarak severim ama K ve V başharflerini nasıl kullansam derken ortaya b&amp;ouml;yle bir şey &amp;ccedil;ıktı. Belki &amp;ccedil;ok sağlam bir protokol &amp;uuml;zerine oturutup, en s&amp;uuml;ratli ve hafifsiklet dağıtık key-value veritabanı olsa meyvenin tonlarında sevimli bir logosu bile olabilirdi. &amp;Ccedil;ok doğal olarak Redis'in milyonda biri kadar iddialı bir &amp;ccedil;alışma değil elbette. Asıl ama&amp;ccedil; &amp;uuml;retim ortamda kullanılabilecek bir şey &amp;uuml;retmek değil, tokio ile asenkron bir &lt;strong&gt;TCP&lt;/strong&gt; sunucusu yazarken &lt;strong&gt;paylaşılan durumu&lt;em&gt;(shared state)&lt;/em&gt;&lt;/strong&gt; nasıl g&amp;uuml;venli şekilde y&amp;ouml;neteceğimizi g&amp;ouml;rmek. Son adımda bunu bir de &lt;strong&gt;Docker&lt;/strong&gt; imajına g&amp;ouml;m&amp;uuml;p herkesin docker run diyerek ayağa kaldırabileceği hale getireceğiz. Asıl testleri kıymetli &lt;strong&gt;Alper Konuralp&lt;/strong&gt; hocam yapacak :D&lt;/p&gt;
&lt;h2&gt;Genel Mimari&lt;/h2&gt;
&lt;p&gt;Projenin genel yapısı olduk&amp;ccedil;a basit ve birka&amp;ccedil; mod&amp;uuml;lden oluşuyor. Bunları aşağıdaki gibi &amp;ouml;zetleyebiliriz.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;main.rs:&lt;/strong&gt; ortam değişkenlerini okuyup sunucuyu ayağa kaldırıyor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;server.rs:&lt;/strong&gt; TCP dinleyicisini&lt;em&gt;(listener)&lt;/em&gt; a&amp;ccedil;ıyor ve gelen her bağlantı i&amp;ccedil;in ayrı bir g&amp;ouml;rev&lt;em&gt;(task)&lt;/em&gt; başlatıyor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;command.rs:&lt;/strong&gt; istemciden gelen metni SET, GET, REMOVE, LIST gibi komutlara d&amp;ouml;n&amp;uuml;şt&amp;uuml;r&amp;uuml;yor&lt;em&gt;(parse)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;handler.rs:&lt;/strong&gt; bir bağlantının yaşam &amp;ouml;mr&amp;uuml; i&amp;ccedil;erisinde gelen komutları işleyip cevabı&lt;em&gt;(response)&lt;/em&gt; yazıyor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;store/data.rs:&lt;/strong&gt; asıl paylaşılan veri yapısı ise burada. Yani key-value &amp;ccedil;iftlerini tutan DataStore bileşenimiz.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Programın &amp;ccedil;alışma akışı kabaca ş&amp;ouml;yle. &lt;strong&gt;main.rs&lt;/strong&gt; i&amp;ccedil;inde &lt;strong&gt;.env&lt;/strong&gt; dosyasından &lt;strong&gt;LISTEN_ADDRESS&lt;/strong&gt; okunuyor ve &lt;strong&gt;server::run&lt;/strong&gt; metodu &amp;ccedil;ağrılıyor. &lt;strong&gt;server.rs&lt;/strong&gt; i&amp;ccedil;indeki d&amp;ouml;ng&amp;uuml; her yeni bağlantıyı kabul ettiğinde &lt;strong&gt;tokio::spawn&lt;/strong&gt; ile ayrı bir asenkron g&amp;ouml;rev başlatılıyor ve o bağlantının okuma/yazma işi &lt;strong&gt;handler::handle_request&lt;/strong&gt;'e devrediliyor. Yani her istemci kendi g&amp;ouml;revinde yaşıyor ama hepsi aynı veri deposuna erişiyor. İşte tam da bu noktada paylaşılan veri&lt;em&gt;(shared data)&lt;/em&gt; problemiyle karşılaşıyoruz.&lt;em&gt;(Ne dediğinizi duyar gibiyim. 100 client bağlansın bu uygulama patlar. Lakin ama&amp;ccedil;lardan birisi de sınırlarda &amp;ccedil;alıştırıp eksiklerimizi g&amp;ouml;rmek ve Redis gibi sistemleri geliştirenlere bir kez daha saygı duymak)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;server mod&amp;uuml;l&amp;uuml;n&amp;uuml;n run fonksiyonu, gelen bağlantıları kabul edip her biri i&amp;ccedil;in ayrı bir g&amp;ouml;rev başlatıyor;&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use crate::handler::handle_request;
use crate::store::DataStore;
use log::info;
use tokio::net::TcpListener;

pub async fn run(address: &amp;amp;str) -&amp;gt; tokio::io::Result&amp;lt;()&amp;gt; {
    let listener = TcpListener::bind(address).await?;
    let store = DataStore::new();

    info!("Server running at {}", address);

    loop {
        let (stream, addr) = listener.accept().await?;
        info!("Client {} connected", addr);
        let store = store.clone();
        tokio::spawn(async move {
            handle_request(stream, store).await;
        });
    }
}&lt;/pre&gt;
&lt;h2&gt;Komut Protokol&amp;uuml;&lt;/h2&gt;
&lt;p&gt;Komutlarımızın &amp;ccedil;alışma protokol&amp;uuml; kasıtlı olarak &amp;ccedil;ok basit tutuldu. Satır satır, boşlukla ayrılmış metin komutları şeklinde ilerliyoruz. &amp;Ouml;rneğin istemci &lt;strong&gt;SET key value&lt;/strong&gt; komutunu g&amp;ouml;nderdiğinde, sunucu key ve value'yu alıp &lt;strong&gt;DataStore&lt;/strong&gt;'a kaydediyor. &lt;strong&gt;GET key&lt;/strong&gt; komutu ile değeri alabiliyor, &lt;strong&gt;REMOVE key&lt;/strong&gt; ile silebiliyor ve &lt;strong&gt;LIST&lt;/strong&gt; komutu ile t&amp;uuml;m anahtarları listeleyebiliyoruz.&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;SET key value
GET key
REMOVE key
LIST&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;command.rs&lt;/strong&gt; i&amp;ccedil;indeki&lt;strong&gt; Command::parse&lt;/strong&gt; fonksiyonu gelen metni en fazla &amp;uuml;&amp;ccedil; par&amp;ccedil;aya b&amp;ouml;l&amp;uuml;yor &lt;em&gt;(splitn(3, ' '))&lt;/em&gt;. B&amp;ouml;ylece value kısmında boşluk ge&amp;ccedil;se bile bozulmuyor.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;#[derive(Debug)]
pub enum Command {
    Set { key: String, value: String },
    Get { key: String },
    Remove { key: String },
    List,
    Invalid(String),
}

impl Command {
    pub fn parse(input: &amp;amp;str) -&amp;gt; Self {
        let mut parts = input.trim().splitn(3, ' ');
        let cmd = parts.next().unwrap_or("").to_uppercase();

        match cmd.as_str() {
            "SET" =&amp;gt; {
                let key = parts.next().unwrap_or("").to_string();
                let value = parts.next().unwrap_or("").to_string();
                Command::Set { key, value }
            }
            "GET" =&amp;gt; {
                let key = parts.next().unwrap_or("").to_string();
                Command::Get { key }
            }
            "REMOVE" =&amp;gt; {
                let key = parts.next().unwrap_or("").to_string();
                Command::Remove { key }
            }
            "LIST" =&amp;gt; Command::List,
            _ =&amp;gt; Command::Invalid(cmd),
        }
    }
}&lt;/pre&gt;
&lt;p&gt;Bilinmeyen bir komut geldiğinde programı patlatmak yerine &lt;strong&gt;Command::Invalid&lt;/strong&gt; d&amp;ouml;n&amp;uuml;yoruz ve hata durumunu tip sisteminin bir par&amp;ccedil;ası haline getiriyoruz. Bu &lt;strong&gt;Rust&lt;/strong&gt; tarafında alıştığımız bir yaklaşım.&lt;/p&gt;
&lt;h2&gt;Paylaşılan Veriyi Y&amp;ouml;netmek: Arc, Mutex, HashMap&lt;/h2&gt;
&lt;p&gt;Gelelim paylaşılan veriyi nasıl y&amp;ouml;neteceğimizi. İşte asıl mevzumuz biraz da bu. &lt;strong&gt;DataStore&lt;/strong&gt; veri yapımız i&amp;ccedil;inde &lt;strong&gt;HashMap&amp;lt;String, String&amp;gt;&lt;/strong&gt; taşıyan ama bunu &lt;strong&gt;Arc&amp;lt;Mutex&amp;lt;...&amp;gt;&amp;gt;&lt;/strong&gt; enstr&amp;uuml;manı ile sarmalayan bir bileşen. Aşağıdaki kod par&amp;ccedil;asında i&amp;ccedil;eriğini bulabilirsiniz.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;

#[derive(Clone)]
#[allow(dead_code)]
pub struct DataStore {
    context: Arc&amp;lt;Mutex&amp;lt;HashMap&amp;lt;String, String&amp;gt;&amp;gt;&amp;gt;,
}

#[allow(dead_code)]
impl DataStore {
    pub fn new() -&amp;gt; Self {
        DataStore {
            context: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    pub async fn set(&amp;amp;self, key: &amp;amp;str, value: &amp;amp;str) {
        let mut context = self.context.lock().await;
        context.insert(key.to_string(), value.to_string());
    }

    pub async fn remove(&amp;amp;self, key: &amp;amp;str) -&amp;gt; bool {
        let mut context = self.context.lock().await;
        context.remove(key).is_some()
    }

    pub async fn get(&amp;amp;self, key: &amp;amp;str) -&amp;gt; Option&amp;lt;String&amp;gt; {
        let context = self.context.lock().await;
        context.get(key).cloned()
    }

    pub async fn keys(&amp;amp;self) -&amp;gt; Vec&amp;lt;String&amp;gt; {
        let context = self.context.lock().await;
        context.keys().cloned().collect()
    }
}&lt;/pre&gt;
&lt;p&gt;Burada &amp;ouml;nemli olan bazı temelleri hatırlamamızda fayda var.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Arc&lt;em&gt;(Atomic Reference Counting)&lt;/em&gt;&lt;/strong&gt;, aynı veriye birden fazla sahibin&lt;em&gt;(owner)&lt;/em&gt; g&amp;uuml;venli bir şekilde referans tutabilmesini sağlıyor. Rust'ın&lt;strong&gt; sahiplik&lt;em&gt;(ownership)&lt;/em&gt;&lt;/strong&gt; modelinde normalde &lt;em&gt;bir değerin tek sahibi olur&lt;/em&gt; kuralı var. &lt;strong&gt;Arc&lt;/strong&gt; bu kuralı, referans sayısını atomik olarak takip ederek gevşetiyor. &lt;strong&gt;server.rs&lt;/strong&gt; i&amp;ccedil;inde her yeni bağlantı i&amp;ccedil;in &lt;strong&gt;store.clone()&lt;/strong&gt; &amp;ccedil;ağrısı yaptığımızda aslında &lt;strong&gt;HashMap&lt;/strong&gt;'in kendisi kopyalanmıyor, sadece referans sayacı artıyor. Referanslar &lt;strong&gt;scope&lt;/strong&gt; dışına &amp;ccedil;ıktığında &lt;strong&gt;Arc&lt;/strong&gt; otomatik olarak sayacı azaltıyor ve son referans silindiğinde veri de serbest bırakılıyor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mutex&lt;em&gt;(Mutual Exclusion)&lt;/em&gt;&lt;/strong&gt; ise aynı anda yalnızca bir &lt;strong&gt;g&amp;ouml;revin&lt;em&gt;(task)&lt;/em&gt; &lt;/strong&gt;veriye erişebileceğini garanti ediyor. &lt;strong&gt;.lock().await&lt;/strong&gt; &amp;ccedil;ağrısı, kilit başka bir g&amp;ouml;rev tarafından tutuluyorsa mevcut g&amp;ouml;revi bekletiyor&lt;em&gt;(ama iş par&amp;ccedil;acığını-thread değil. Asenkron bir bekleme s&amp;ouml;z konusu burada)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Uygulamada dikkat edilmesi gereken bir ayrıntı var. Kullanılan &lt;strong&gt;Mutex&lt;/strong&gt; enstr&amp;uuml;manı dikkat edileceği &amp;uuml;zere &lt;strong&gt;std::sync::Mutex&lt;/strong&gt; değil, &lt;strong&gt;tokio::sync::Mutex&lt;/strong&gt;. Standart k&amp;uuml;t&amp;uuml;phane ile gelen primitive'lerden olan &lt;strong&gt;Mutex&lt;/strong&gt; senkron &amp;ccedil;alışıyor ve bu nedenle kilidi tutarken bir &lt;strong&gt;.await&lt;/strong&gt; noktasına girilirse o kilidi tutan &lt;strong&gt;iş par&amp;ccedil;acığı&lt;em&gt;(thread)&lt;/em&gt;&lt;/strong&gt; bloke oluyor ve bu da &lt;strong&gt;tokio&lt;/strong&gt;'nun &lt;strong&gt;runtime&lt;/strong&gt;'ında beklenmedik tıkanmalara yol a&amp;ccedil;abilir. &lt;strong&gt;tokio::sync::Mutex&lt;/strong&gt; ise tamamen asenkron &amp;ccedil;alışma moduna uyumlu şekilde yazılmış. Kilit meşgulken bekleyen g&amp;ouml;rev, iş par&amp;ccedil;acığını serbest bırakıp runtime'ın başka g&amp;ouml;revleri &amp;ccedil;alıştırmasına izin veriyor. Bu projede kilit tutulan bloklar zaten &amp;ccedil;ok kısa&lt;strong&gt;&lt;em&gt;(HashMap &amp;uuml;zerinde insert/get/remove)&lt;/em&gt;&lt;/strong&gt; olduğu i&amp;ccedil;in pratikte farkın k&amp;uuml;&amp;ccedil;&amp;uuml;k olacağını d&amp;uuml;ş&amp;uuml;nebiliriz ama alışkanlık olarak asenkron kod i&amp;ccedil;inde asenkron kilit kullanmak daha doğrudur. Tabii bunu bir denemenizi ve ger&amp;ccedil;eken dediğim gibi olup olmadığını ispatlamanızı şiddetle tavsiye ederim ;)&lt;/p&gt;
&lt;h2&gt;Bağlantı Başına Bir G&amp;ouml;rev Meselesi&lt;em&gt;(Task)&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;tokio::spawn, işletim sistemi seviyesinde yeni bir &lt;strong&gt;iş par&amp;ccedil;acığı&lt;em&gt;(thread)&lt;/em&gt;&lt;/strong&gt; a&amp;ccedil;maz. &lt;strong&gt;Tokio&lt;/strong&gt;'nun kendi &lt;strong&gt;&amp;ccedil;ok iş par&amp;ccedil;acıklı&lt;em&gt;(multi-threaded)&lt;/em&gt; zamanlayıcısında&lt;em&gt;(scheduler)&lt;/em&gt;&lt;/strong&gt; &amp;ccedil;alışacak hafif bir g&amp;ouml;rev&lt;em&gt;(task - ki sanırım buna bazen "green thread" de deniyor)&lt;/em&gt; oluşturur. Y&amp;uuml;zlerce istemci bağlansa bile işletim sistemi bunun y&amp;uuml;k&amp;uuml;n&amp;uuml; hissetmeyecektir &amp;ccedil;&amp;uuml;nk&amp;uuml; tokio bu g&amp;ouml;revleri az sayıda ger&amp;ccedil;ek iş par&amp;ccedil;acığı &amp;uuml;zerinde zamanlar. DataStore &amp;uuml;zerindeki &lt;strong&gt;Arc&amp;lt;Mutex&amp;lt;...&amp;gt;&amp;gt;&lt;/strong&gt; sayesinde de bu g&amp;ouml;revlerin hepsi aynı veriye tutarlı bi&amp;ccedil;imde erişebilir.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;handler.rs&lt;/strong&gt; i&amp;ccedil;indeki handle_request fonksiyonu, bağlantı kapanana kadar d&amp;ouml;ng&amp;uuml;de okuma yapıp gelen komutu &lt;strong&gt;DataStore&lt;/strong&gt; &amp;uuml;zerinde &amp;ccedil;alıştırır ve cevabı istemci tarafa yine &lt;strong&gt;TCP stream&lt;/strong&gt; &amp;uuml;zerinde yazar.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use crate::command::Command;
use crate::store::DataStore;
use log::{error, info};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;

#[allow(dead_code)]
pub async fn handle_request(mut stream: TcpStream, data_store: DataStore) {
    let mut buffer = [0; 1024];
    loop {
        let size = match stream.read(&amp;amp;mut buffer).await {
            Ok(0) =&amp;gt; break,
            Ok(n) =&amp;gt; n,
            Err(e) =&amp;gt; {
                error!("{}", e);
                break;
            }
        };

        info!("Read {}(bytes)", size);

        let request = String::from_utf8_lossy(&amp;amp;buffer[..size]);
        let cmd = Command::parse(&amp;amp;request);

        let response = match cmd {
            Command::Set { key, value } =&amp;gt; {
                data_store.set(&amp;amp;key, &amp;amp;value).await;
                "OK\r\n".to_string()
            }
            Command::Get { key } =&amp;gt; {
                info!("GET {}", key);
                match data_store.get(&amp;amp;key).await {
                    Some(value) =&amp;gt; format!("{}\r\n", value),
                    None =&amp;gt; "NOT FOUND\r\n".to_string(),
                }
            }
            Command::Remove { key } =&amp;gt; {
                info!("REMOVE {}", key);
                if data_store.remove(&amp;amp;key).await {
                    "OK\r\n".to_string()
                } else {
                    "NOT FOUND\r\n".to_string()
                }
            }
            Command::List =&amp;gt; {
                let keys = data_store.keys().await;
                format!("{}\r\n", keys.join("\r\n"))
            }
            Command::Invalid(cmd) =&amp;gt; format!("ERROR: Unknown command '{}'\r\n", cmd),
        };

        if let Err(e) = stream.write_all(response.as_bytes()).await {
            error!("{}", e);
            break;
        }
    }
}&lt;/pre&gt;
&lt;p&gt;Sabit boyutlu &lt;strong&gt;1024&lt;/strong&gt; byte veri taşıyacak bir &lt;strong&gt;tampon&lt;em&gt;(buffer)&lt;/em&gt;&lt;/strong&gt; kullanılıyoruz. K&amp;uuml;&amp;ccedil;&amp;uuml;k komutlar i&amp;ccedil;in yeterlidir ama ger&amp;ccedil;ek bir &amp;uuml;retim sistemi i&amp;ccedil;in elbette değişken uzunluklu ya da &lt;strong&gt;uzunluk-&amp;ouml;ncelikli&lt;em&gt;(length-prefixed)&lt;/em&gt;&lt;/strong&gt; bir protokol tercih edilebilir. Bu proje kapsamında ama&amp;ccedil; protokol tasarımı değil, eşzamanlılık pratiği olduğu i&amp;ccedil;in basit tutuldu&amp;nbsp;&lt;strong&gt;&lt;em&gt;(Cidden bu ama&amp;ccedil;la kendi protokol&amp;uuml;m&amp;uuml; yazacak seviyede bilgim olsun isterdim)&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Testler&lt;/h2&gt;
&lt;p&gt;Repoda hem &lt;strong&gt;DataStore&lt;/strong&gt; hem de &lt;strong&gt;Command::parse&lt;/strong&gt; i&amp;ccedil;in birka&amp;ccedil; birim testimiz var esasında. &lt;strong&gt;DataStore&lt;/strong&gt; testinde bir anahtar-değer &amp;ccedil;ifti ekleyip sonra onu alıyoruz ve beklenen değerle karşılaştırıyoruz. &lt;strong&gt;Command::parse&lt;/strong&gt; testinde ise bir metin komutunu parse edip doğru şekilde ayrıştırıldığını kontrol ediyoruz. Başka birim testler de eklenip kodun g&amp;uuml;venilirliği artırılabilir.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;#[tokio::test]
async fn test_set_and_get() {
    let data_store = DataStore::new();
    data_store.set("Resilience", "on").await;
    let expected = data_store.get("Resilience").await.unwrap();
    assert_eq!(expected, "on");
}

#[test]
fn test_command_parse() {
    let cmd = Command::parse("SET Connection dataSource=localhost;database=MongoDb");
    match cmd {
        Command::Set { key, value } =&amp;gt; {
            assert_eq!(key, "Connection");
            assert_eq!(value, "dataSource=localhost;database=MongoDb");
        }
        _ =&amp;gt; panic!("Expected to parse SET command!"),
    }
}&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;#[tokio::test]&lt;/strong&gt; makrosunun burada işaret ettiği &amp;ouml;nemli bir nokta var. &lt;strong&gt;DataStore&lt;/strong&gt; asenkron metotlar barındırdığı i&amp;ccedil;in, testin kendisinin de bir &lt;strong&gt;tokio runtime&lt;/strong&gt; i&amp;ccedil;inde &amp;ccedil;alışması gerekiyor. Sıradan bir &lt;strong&gt;#[test]&lt;/strong&gt; ile &lt;strong&gt;.await&lt;/strong&gt; kullanmaya &amp;ccedil;alışsaydık derleyici bunu kabul etmeyecektir. Testleri &amp;ccedil;alıştırmak i&amp;ccedil;in bildiğiniz &amp;uuml;zere cargo test komutunu kullanıyoruz.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Eylul/KiviStore_00.png" alt="" /&gt;&lt;/p&gt;
&lt;h2&gt;Docker Imajını Oluşturmak&lt;/h2&gt;
&lt;p&gt;&amp;Ccedil;alışmamızın son adımında projeyi herkesin kolayca &amp;ccedil;alıştırabileceği bir hale getireceğiz. Burada ama&amp;ccedil; uygulamayı container olarak da sunabilmek. Bunun i&amp;ccedil;in bilindiği &amp;uuml;zere bir &lt;strong&gt;Dockerfile&lt;/strong&gt; dosyasına ihtiyacımız var. &lt;strong&gt;İki aşamalı&lt;em&gt;(multi-stage)&lt;/em&gt;&lt;/strong&gt; bir yapı kullanıyoruz. İlk aşamada &lt;strong&gt;rust:1.87.0&lt;/strong&gt; imajı &amp;uuml;zerinden cargo build &lt;strong&gt;--release&lt;/strong&gt; ile derleme yapılıyor. İkinci aşamadaysa sadece derlenmiş binary &amp;ccedil;ok daha k&amp;uuml;&amp;ccedil;&amp;uuml;k olan debian:bookworm-slim imajına kopyalanıyor.&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;FROM rust:1.87.0 AS builder

WORKDIR /app
COPY . .

RUN cargo build --release

FROM debian:bookworm-slim

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y ca-certificates &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /app/target/release/kivi-store /usr/local/bin/kivi-store
COPY .env .env

EXPOSE 5555

CMD ["kivi-store"]&lt;/pre&gt;
&lt;p&gt;Bu sayede son imaj &lt;strong&gt;Rust&lt;/strong&gt; derleyicisini ve ara derleme dosyalarını i&amp;ccedil;ermiyor. Onun yerine &amp;ccedil;alıştırılabilir dosyayı ve gerekli sertifikaları taşıyor. &lt;strong&gt;docker-compose.yml&lt;/strong&gt; ise tek servisli, minimal bir tanımlamadan ibaret.&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;services:
  kivi-store:
    build: .
    ports:
      - "5555:5555"&lt;/pre&gt;
&lt;p&gt;İmajı build etmek ve konteynırımızı ayağa kaldırmak i&amp;ccedil;in aşağıdaki komutları deneyebiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;# Docker imajını oluşturmak
docker build -t kivi-store .

# docker-compose ile ayağa kaldırmak i&amp;ccedil;in
docker-compose up -d

# Eğer kodda bir değişiklik yaparsak container imajını tazelemek gerekir
docker-compose up -d --build&lt;/pre&gt;
&lt;h2&gt;Ger&amp;ccedil;ek Denemeler&lt;/h2&gt;
&lt;p&gt;Sunucu ayağa kalktıktan sonra herhangi bir &lt;strong&gt;TCP&lt;/strong&gt; istemcisiyle deneme yapabiliriz ancak en g&amp;uuml;zeli kendi istemcimizi sunmak olacaktır. Bu ama&amp;ccedil;la bin klas&amp;ouml;r&amp;uuml;nde aşağıdaki i&amp;ccedil;eriği sahip programı yazalım.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use std::env;
use std::io::{self, Write};
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;

#[tokio::main]
async fn main() -&amp;gt; tokio::io::Result&amp;lt;()&amp;gt; {
    let address = env::var("KIVI_ADDRESS").unwrap_or_else(|_| "127.0.0.1:5555".to_string());
    let mut stream = TcpStream::connect(&amp;amp;address).await?;
    let mut stdin = BufReader::new(tokio::io::stdin()).lines();

    println!("kivi-store istemcisi - {} adresine bağlandı", address);
    println!("Komutlar: SET key value | GET key | REMOVE key | LIST (&amp;ccedil;ıkmak i&amp;ccedil;in Ctrl+C)\n");

    loop {
        print!("&amp;gt; ");
        io::stdout().flush()?;

        let line = match stdin.next_line().await? {
            Some(line) if !line.trim().is_empty() =&amp;gt; line,
            Some(_) =&amp;gt; continue,
            None =&amp;gt; break,
        };

        stream.write_all(format!("{}\r\n", line).as_bytes()).await?;

        let mut buffer = [0u8; 1024];
        let n = stream.read(&amp;amp;mut buffer).await?;
        if n == 0 {
            println!("Bağlantı sunucu tarafından kapatıldı.");
            break;
        }
        print!("{}", String::from_utf8_lossy(&amp;amp;buffer[..n]));
    }

    Ok(())
}&lt;/pre&gt;
&lt;p&gt;İstemci taraf, kivi-store protokol&amp;uuml;n&amp;uuml;n doğası gereği&lt;em&gt;(bir istek - request -&amp;gt; bir cevap - response)&lt;/em&gt; tamamen senkron bir d&amp;ouml;ng&amp;uuml;yle &amp;ccedil;alışır. Satırı okur ve sonuna &lt;strong&gt;\r\n&lt;/strong&gt; ekleyip g&amp;ouml;nderir. Ardından cevabı bekler ve sonucu ekrana basar. &lt;strong&gt;tokio::io::stdin()&lt;/strong&gt;'in asenkron satır okuma desteği zaten &lt;strong&gt;k&amp;uuml;fenin&lt;em&gt;(crate)&lt;/em&gt;&lt;/strong&gt; full &lt;strong&gt;&amp;ouml;zellik k&amp;uuml;mesiyle&lt;em&gt;(feature set)&lt;/em&gt;&lt;/strong&gt; geliyor, dolayısıyla ekstra bir bağımlılık eklemeye de gerek yok.&lt;/p&gt;
&lt;p&gt;Şimdi terminalden testlerimize başlayabiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;# Docker imajı ile veya cargo run ile sunucuyu ayağa kaldırdıktan sonra
# İstemciyi işletebiliriz
cargo run --bin client

# İşte birka&amp;ccedil; &amp;ouml;rnek komut
SET Language Rust
SET Framework Tokio
LIST
SET System Linux
GET Language
REMOVE Language
GET Language
NOT FOUND
LIST&lt;/pre&gt;
&lt;p&gt;İşte &amp;ccedil;alışma zamanından bir ekran g&amp;ouml;r&amp;uuml;nt&amp;uuml;s&amp;uuml;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Eylul/KiviStore_01.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Aynı &amp;ouml;rneği &lt;strong&gt;docker-compose&lt;/strong&gt; &amp;uuml;zerinden ayağa kaldırıp denediğimizde de benzer sonu&amp;ccedil;ları almamız gerekiyor.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Eylul/KiviStore_02.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Bu arada kendi istemcimizi denedik ama şart değil. &lt;strong&gt;ncat&lt;/strong&gt; gibi bir aracı da kullanabiliriz. Ben &lt;strong&gt;Windows 11&lt;/strong&gt; sistemine &lt;strong&gt;chocolatey&lt;/strong&gt; ile &lt;strong&gt;ncat&lt;/strong&gt; kurdum ve aşağıdaki gibi denedim.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;# kurmak i&amp;ccedil;in
# choco install nmap

# bağlanmak i&amp;ccedil;in
ncat localhost 5555

# sonraki komutlar aynı şekilde &amp;ccedil;alışıyor&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Eylul/KiviStore_03.png" alt="" /&gt;&lt;/p&gt;
&lt;h2&gt;Dağıtım&lt;em&gt;(Deployment)&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;Diyelimki kıymetli Alper Konuralp hocam ile birlikte yaptığımız yayında bu sunucu uygulamasını docker paketi olarak verip denemesini istiyeceğim. Hoş &amp;ouml;rneği &lt;strong&gt;docker-hub&lt;/strong&gt;'a y&amp;uuml;kleyip &lt;strong&gt;docker pull&lt;/strong&gt; ile &amp;ccedil;ekmesini de sağlayabilirim. Ama buna gerek duyulmayacak kadar sıradan bir &amp;ouml;rneğimiz var. Aşağıdaki komutlarla ilerleyelim.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;# &amp;Ouml;nce temiz bir build alalım
docker build --no-cache -t kivi-store:v0.1.0 .

# Şimdi imajımızı tek bir dosyaya paketleyelim
docker save -o kivi-store-v0.1.0.tar kivi-store:v0.1.0

# Boyut b&amp;uuml;y&amp;uuml;k m&amp;uuml;? Hi&amp;ccedil; dert değil sıkıştırıp transfer boyutunu ufaltabiliriz de
# Gzip yoksa y&amp;uuml;klemek gerekir. Bende yoktu &amp;ccedil;ikolata yardımıma yetişti
# choco install gzip
docker save kivi-store:v0.1.0 | gzip &amp;gt; kivi-store-v0.1.0.tar.gz

# Alper Hocam'a bu &amp;ccedil;ıktıyı ulaştırdıktan sonra onun da yapması gerekenler var tabii
# &amp;Ouml;nce imajı y&amp;uuml;klemeli
docker load -i kivi-store-v0.1.0.tar
# Eğer zip'li dosya ile g&amp;ouml;nderildiysek
# gunzip -c kivi-store-v0.1.0.tar.gz | docker load

# Sonra Container'ı ayağa kaldırabilir
docker run -d -p 5555:5555 --name kivi-store kivi-store:v0.1.0&lt;/pre&gt;
&lt;p&gt;Bakalım neler olacak?&lt;/p&gt;
&lt;h2&gt;Sonu&amp;ccedil;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;kivi-store&lt;/strong&gt;, iddialı bir proje olma amacı taşımıyor; amacı, &lt;strong&gt;Arc&amp;lt;Mutex&amp;lt;...&amp;gt;&amp;gt;&lt;/strong&gt; ikilisinin neden birlikte kullanıldığını, &lt;strong&gt;tokio::spawn&lt;/strong&gt; ile a&amp;ccedil;ılan g&amp;ouml;revlerin işletim sistemi iş par&amp;ccedil;acıklarından farkını ve asenkron kod i&amp;ccedil;inde neden asenkron bir kilide ihtiya&amp;ccedil; duyduğumuzu elle deneyerek pekiştirmekti. Ortaya &amp;ccedil;ıkan sonu&amp;ccedil;, birka&amp;ccedil; y&amp;uuml;z satırlık k&amp;uuml;&amp;ccedil;&amp;uuml;k bir &lt;strong&gt;TCP&lt;/strong&gt; sunucusu ama i&amp;ccedil;inde &lt;strong&gt;paylaşılan durumu&lt;em&gt;(shared state)&lt;/em&gt;&lt;/strong&gt; g&amp;uuml;venli y&amp;ouml;netmenin temel desenini barındırıyor.&lt;/p&gt;
&lt;p&gt;Bir sonraki adım olarak aklımda şunlar var: &lt;strong&gt;uzunluk-&amp;ouml;ncelikli&lt;em&gt;(length-prefixed)&lt;/em&gt; &lt;/strong&gt;bir protokole ge&amp;ccedil;mek, &lt;strong&gt;RwLock&lt;/strong&gt; ile okuma-ağırlıklı senaryoları karşılaştırmak ve belki de basit bir &lt;strong&gt;TTL&lt;em&gt;(time-to-live)&lt;/em&gt;&lt;/strong&gt; desteği eklemek. B&amp;ouml;ylece geldik derlenmiş bir maceramızın daha sonuna. Umarım faydalı olmuştur. Tekrardan g&amp;ouml;r&amp;uuml;ş&amp;uuml;nceye dek hepinize mutlu g&amp;uuml;nler dilerim.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/buraksenyurt/rust-farm/tree/main/handson/kivi-store" target="_blank"&gt;&amp;Ouml;rnek kodlara github &amp;uuml;zerinden ulaşmak i&amp;ccedil;in tıklayın&lt;/a&gt;&lt;/p&gt;</summary>
    <published>2026-09-01T18:54:00+00:00</published>
    <link rel="related" href="https://www.buraksenyurt.com/post/rust-ile-basit-bir-key-value-store-uygulamasi#comment" />
    <category term="Rust" />
    <betag:tag>rust</betag:tag>
    <betag:tag>rust programming language</betag:tag>
    <betag:tag>mutex</betag:tag>
    <betag:tag>concurrency</betag:tag>
    <betag:tag>concurrent programming</betag:tag>
    <betag:tag>key value storage</betag:tag>
    <betag:tag>docker</betag:tag>
    <betag:tag>container</betag:tag>
    <dc:publisher>bsenyurt</dc:publisher>
    <dc:description>Bir programlama dilini öğrenmeye çalışırken bazı kavramları anlamanın en iyi yolu elbette senaryo bazlı pratik yapmaktan geçiyor. Yazıyı yazdığım tarih itibariyle şöyle bir bakıyorum da repolarımın kıyısında köşesinde bu amaçla yazılmış ama unutulmuş denemeler var. Bugün onlardan birisini yazıya döküp tekrar etmeye çalışacağım. Elimdeki konu oldukça sağlam. Çok sık karşımıza çıkan ama üzerine fazla kafa yormadan ilerlediğimizde bize dert çıkarabilecek üç önemli parçası var; eşzamanlılık(concurrency), müşterek kilitleme(mutex) ve çok kullanıcı tarafından ortaklaşa paylaşılmak istenen veri(shared data). Bunları teorik olarak bilmek başka, aynı verinin birden fazla bağlantı tarafından aynı anda değiştirilmeye çalışıldığı bir senaryoda ele alıp test etmek bambaşka. O yüzden basit ama gerçekçi bir problem seçtim; TCP protokolü üzerinden konuşan, verileri bellekte tutan, minik bir key-value store yazmak.</dc:description>
    <pingback:server>https://www.buraksenyurt.com/pingback.axd</pingback:server>
    <pingback:target>https://www.buraksenyurt.com/post.aspx?id=9fbc6222-c3ec-40fc-9f9a-13ef9c1eff40</pingback:target>
    <slash:comments>0</slash:comments>
    <trackback:ping>https://www.buraksenyurt.com/trackback.axd?id=9fbc6222-c3ec-40fc-9f9a-13ef9c1eff40</trackback:ping>
    <wfw:comment>https://www.buraksenyurt.com/post/rust-ile-basit-bir-key-value-store-uygulamasi#comment</wfw:comment>
    <wfw:commentRss>https://www.buraksenyurt.com/syndication.axd?post=9fbc6222-c3ec-40fc-9f9a-13ef9c1eff40</wfw:commentRss>
  </entry>
  <entry>
    <id>https://www.buraksenyurt.com/post/tauri-ile-masaustu-uygulama-gelistirme</id>
    <title>Tauri ile Masaüstü Uygulama Geliştirme</title>
    <updated>2026-08-31T07:45:00+00:00</updated>
    <link rel="self" href="https://www.buraksenyurt.com/post.aspx?id=47603d9d-a454-4b8a-96b1-5844f1825e63" />
    <link href="https://www.buraksenyurt.com/post/tauri-ile-masaustu-uygulama-gelistirme" />
    <author>
      <name>bsenyurt</name>
    </author>
    <summary type="html">&lt;p&gt;Mesleğe ilk başladığım yıllarda Web teknolojileri daha yeni yeni yaygınlaşmaya başlamıştı. Java ilk s&amp;uuml;r&amp;uuml;m&amp;uuml;n&amp;uuml; yayınlamışken ben daha &amp;ccedil;ok Delphi ile Windows platformunda masa&amp;uuml;st&amp;uuml; uygulamalar geliştirmekle meşguld&amp;uuml;m. Web tabanlı geliştirmenin avantajları bir yana masa&amp;uuml;st&amp;uuml; uygulamalarda konuk olunan işletim sisteminin g&amp;uuml;c&amp;uuml;nden yararlanma, state tutma şansına sahip olabiliyorduk. Elbette zaman i&amp;ccedil;erisinde neredeyse t&amp;uuml;m iş modelleri web tabanlı &amp;ccedil;&amp;ouml;z&amp;uuml;mlere ge&amp;ccedil;ti. Web 1.0, 2.0, 3.0 derken Internet'in neredeyse t&amp;uuml;m altyapısına sızan sayısız JavaScript Framework arasında ge&amp;ccedil;iş yapar olduk. &amp;Ouml;zellikle cross-platform tabanlı sistemleri d&amp;uuml;ş&amp;uuml;n&amp;uuml;nce karşımıza farklı hibrit &amp;ccedil;&amp;ouml;z&amp;uuml;mler de &amp;ccedil;ıktı. Chromium gibi tarayıcıların işletim sisteminde &amp;ccedil;alışabilen &amp;ccedil;ekirdek ara y&amp;uuml;zleri ile backend tarafı klasik servis haberleşmeleri &amp;uuml;zerinden konuşmaya başladı. Herhalde Electron bu &amp;ccedil;&amp;ouml;z&amp;uuml;mler arasında en bilinenleri arasındadır. Tabii farklı dillerin sunduğu farklı &amp;ccedil;&amp;ouml;z&amp;uuml;mler de var. &amp;Ouml;rneğin Rust cephesindeki yaklaşımların başında Tauri geliyor. Bir s&amp;uuml;redir kenara park ettiğim bu konuyu nihayet deneme fırsatı buldum ve &amp;ouml;ğrendiklerimi bir yazıda toplamak istedim. Amacımız sysinfo k&amp;uuml;fesini(crate) kullanarak ilkel bir dashboard hazırlamak ve burada en azından CPU, RAM ve Disk kullanımını g&amp;ouml;stermek.&lt;/p&gt;
&lt;h2&gt;Tauri &amp;Ccedil;alışma Modeli&lt;/h2&gt;
&lt;p&gt;Bir Tauri uygulaması aslında tek bir process'te &amp;ccedil;alışan iki uygulamadan oluşur. &amp;Ccedil;ekirdek fonksiyonellikler Rust tarafında ele alınırken &amp;ouml;ny&amp;uuml;z tarafında WebView kullanılır. Bu WebView her platformda farklıdır; Linux tarafında WebKitGTK, Windows tarafında Edge WebView2 ve macOS tarafında WKWebView. İşte Electron ile kıyaslandığında ilk fark tam olarak burada ortaya &amp;ccedil;ıkar. Electron kendi Chromium motorunu paketin i&amp;ccedil;inde taşırken, Tauri işletim sisteminde zaten var olan WebView bileşenini kullanır. Bu da paket boyutunda ve bellek t&amp;uuml;ketiminde ciddi bir fark yaratır.&lt;/p&gt;
&lt;p&gt;Ele alacağımız &amp;ouml;rneği g&amp;ouml;z &amp;ouml;n&amp;uuml;ne alırsak Tauri'nin &amp;ccedil;alışma prensibini aşağıdaki şekildeki gibi &amp;ouml;zetleyebiliriz.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Agustos/Architecture.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;&amp;Ouml;ny&amp;uuml;z ve arka plan uygulamaları arasında IPC(Inter Process Communication) mekanizması ile veri alışverişi yapılır. REST gibi network katmanına &amp;ccedil;ıkmayı gerektirecek bir iletişim s&amp;ouml;z konusu değildir, doğrudan IPC'nin avantajlarından yararlanılır. &amp;Ouml;ny&amp;uuml;z i&amp;ccedil;in s&amp;ouml;yleyebileceğimiz &amp;ouml;nemli detaylardan birisi de işletim sistemine doğrudan temas etmiyor oluşudur. İhtiya&amp;ccedil; duyduğu her şeyi Rust tarafından ister. Bu iletişim de temelde iki enstr&amp;uuml;man &amp;uuml;zerine kuruludur.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Commands: Javascript veya Typescript tarafından invoke fonksiyonu ile `tauri::command` direktifi ile işaretlenmiş Rust fonksiyonları &amp;ccedil;ağırılabilir. `Request/Response` işleyişi her zaman asenkrondur.&lt;/li&gt;
&lt;li&gt;Events: Fire and forget mantığında one-to-many şeklinde bir iletişim s&amp;ouml;z konusudur. Rust tarafından `tauri::event::emit` ile event yayınlanabilir ve &amp;ouml;ny&amp;uuml;z tarafında `window.listen` ile yakalanabilir.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Aradaki iletişimde hareket eden bilgilerin JSON formatında serileşebilir olması gerekir. Bu da i&amp;ccedil;eride serde k&amp;uuml;fesinin kullanıldığı anlamına gelir ki yazının ilerleyen kısımlarında bunu bolca g&amp;ouml;receğiz.&lt;/p&gt;
&lt;h2&gt;Kurulumlar&lt;/h2&gt;
&lt;p&gt;Ben &amp;ouml;rnek &amp;ccedil;alışmayı Ubuntu 26.04 &amp;uuml;zerinde ger&amp;ccedil;ekleştirdim. Sistemimde zaten Rust, Node.js ve npm kurulu. Ancak bunların haricinde &amp;ouml;zellikle Linux tarafında `libwebkit2gtk-4.1-dev` paketinin de kurulu olması gerekiyor. Bu paket WebKitGTK'nın geliştirme k&amp;uuml;t&amp;uuml;phanesini i&amp;ccedil;eriyor. Windows tarafında kullanılan WebView2 i&amp;ccedil;in ayrıca bir kurulum yapmaya gerek yok, g&amp;uuml;ncel Windows s&amp;uuml;r&amp;uuml;mleriyle birlikte geliyor. macOS tarafında ise WKWebView zaten sistemin bir par&amp;ccedil;ası.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;# &amp;Ouml;ncelikle gerekli bazı ortam k&amp;uuml;t&amp;uuml;phanelerini y&amp;uuml;kleyelim.
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev \
    build-essential \
    curl \
    wget \
    file \
    libxdo-dev \
    libssl-dev \
    libayatana-appindicator3-dev \
    librsvg2-dev

# Tauri'nin kendisi Node'a ihtiya&amp;ccedil; duymaz fakat CLI scaffolding ve Vite dev server ihtiya&amp;ccedil; duyar.
# Bu nedenle her ihtimale karşı uyumlu bir node versiyonu kurmakta fayda var. Ben nvm kullandım.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts

# Hemen bir kontrol
node --version&lt;/pre&gt;
&lt;p&gt;Bu paketlerin ne işe yaradığına kısaca bakmakta yarar var. Zira ilk defa karşılaşan birisi i&amp;ccedil;in `apt install` satırındaki liste biraz kalabalık g&amp;ouml;r&amp;uuml;nebilir.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`libwebkit2gtk-4.1-dev` : WebKitGTK'nın geliştirme k&amp;uuml;t&amp;uuml;phanesi. Bir başka deyişle uygulama i&amp;ccedil;in &amp;ouml;nemli olan WebView'ın ta kendisi.&lt;/li&gt;
&lt;li&gt;`build-essential` : Rust i&amp;ccedil;in C toolchain linker.&lt;/li&gt;
&lt;li&gt;`libxdo-dev` : X11 i&amp;ccedil;in mouse ve keyboard event'lerini sim&amp;uuml;le etmek i&amp;ccedil;in gerekli k&amp;uuml;t&amp;uuml;phane.&lt;/li&gt;
&lt;li&gt;`libssl-dev` : OpenSSL k&amp;uuml;t&amp;uuml;phanesi. Rust tarafında bazı kriptografik fonksiyonlar i&amp;ccedil;in gerekli.&lt;/li&gt;
&lt;li&gt;`libayatana-appindicator3-dev` : Linux tarafında uygulama ikonları i&amp;ccedil;in gerekli k&amp;uuml;t&amp;uuml;phane. System tray desteği.&lt;/li&gt;
&lt;li&gt;`librsvg2-dev` : SVG ikonlarının render edilmesi i&amp;ccedil;in gerekli k&amp;uuml;t&amp;uuml;phane. Tauri uygulamaları SVG ikonlarını kullanır.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Projenin Oluşturulması ve İlk G&amp;ouml;sterim&lt;/h2&gt;
&lt;p&gt;Projeyi kurmak olduk&amp;ccedil;a basit. Scaffolding i&amp;ccedil;in Tauri CLI aracını kullanıyoruz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;npm create tauri-app@latest&lt;/pre&gt;
&lt;p&gt;CLI arabirimi uygulama ile ilgili bize birka&amp;ccedil; soru soracaktır. Bu soruları aşağıdaki gibi cevaplayabiliriz.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Project name: sys-trace(Bu benim verdiğim uygulama adı. Siz başka bir tane verebilirsiniz)&lt;/li&gt;
&lt;li&gt;Identifier: com.buraks.sys-trace(Varsayılan olarak sunulanı kabul ettim. Bana biraz Java classpath mantığını hatırlattı)&lt;/li&gt;
&lt;li&gt;Choose which language to use for the frontend: Typescript&lt;/li&gt;
&lt;li&gt;Choose your package manager: npm&lt;/li&gt;
&lt;li&gt;Choose your UI template: Vanilla&lt;/li&gt;
&lt;li&gt;Choose your UI flavor: Typescript&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bilerek herhangi bir framework se&amp;ccedil;medim. Zira odağımızın React veya Svelte gibi bir tarafa kaymasını şu an i&amp;ccedil;in gerek yok. Bu bizim bir nevi *Hello Tauri* uygulamamız. Sonrasında uygulama klas&amp;ouml;r&amp;uuml;ne girip gerekli npm paketlerini y&amp;uuml;kleyebilir ve uygulamanın ilk halini &amp;ccedil;alıştırabiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;cd sys-trace
npm install

# Uygulamayı &amp;ccedil;alıştıralım.
npm run tauri dev
&lt;/pre&gt;
&lt;p&gt;Karşımıza aşağıdaki gibi bir pencere &amp;ccedil;ıkması lazım.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Agustos/Runtime_00.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;İlk &amp;ccedil;alıştırmada b&amp;uuml;y&amp;uuml;k ihtimalle Rust tarafı gerekli k&amp;uuml;feleri(crates) y&amp;uuml;kleyecektir ve bu işlem birka&amp;ccedil; dakika s&amp;uuml;rebilir. Sabırlı olun :D&lt;/p&gt;
&lt;h2&gt;Proje İskeleti Hakkında Bilgi&lt;/h2&gt;
&lt;p&gt;Uygulama &amp;ccedil;alıştığına g&amp;ouml;re proje i&amp;ccedil;eriğini biraz tanıyalım. Tabii sonraki s&amp;uuml;r&amp;uuml;mlerde değişiklikler olabilir.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`index.html` : WebView giriş noktası(entry point)*.&lt;/li&gt;
&lt;li&gt;`vite.config.ts` : Tauri development server i&amp;ccedil;in Vite konfig&amp;uuml;rasyonu.&lt;/li&gt;
&lt;li&gt;`src` klas&amp;ouml;r&amp;uuml;: Tamamen &amp;ouml;ny&amp;uuml;z tarafı.&lt;/li&gt;
&lt;li&gt;`src-tauri` klas&amp;ouml;r&amp;uuml;: Rust tarafı ve ayrı bir crate. Kendi `Cargo.toml` dosyasına sahiptir. Dolayısıyla bu &amp;ouml;rnekte kullanacağımız sysinfo k&amp;uuml;t&amp;uuml;phanesi de buraya eklenir.&lt;/li&gt;
&lt;li&gt;`src-tauri/capabilities` klas&amp;ouml;r&amp;uuml;: Tauri uygulamasının hangi yetenekleri kullanacağını belirten manifest dosyaları(Permission grants)*.&lt;/li&gt;
&lt;li&gt;`lib.rs` ve `main.rs` : İş kurallarımız burada yaşar.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust tarafında programın &amp;ccedil;alıştığı sistemden bazı bilgileri toplamak i&amp;ccedil;in kullanılan pop&amp;uuml;ler k&amp;uuml;felerden birisi sysinfo crate'idir. Bunu `src-tauri` klas&amp;ouml;r&amp;uuml;nde aşağıdaki gibi y&amp;uuml;kleyebiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;cd src-tauri
cargo add sysinfo --no-default-features --features system,disk&lt;/pre&gt;
&lt;p&gt;Buradaki `--no-default-features` kullanımı bilin&amp;ccedil;li bir tercih. sysinfo varsayılan olarak process listesi, network arabirimleri, sıcaklık sens&amp;ouml;rleri gibi pek &amp;ccedil;ok bileşeni de derlemeye dahil ediyor. Bizim ihtiyacımız sadece sistem ve disk bilgileri olduğundan gereksiz y&amp;uuml;kten kurtulmuş oluyoruz. Sonu&amp;ccedil;ta kullanmayacağımız enstr&amp;uuml;manları paket i&amp;ccedil;erisine almak gereksiz bir şişmeye de neden olacaktır. Bu işlemler sonrasında `Cargo.toml` dosyasında gerekli bağımlılıkları g&amp;ouml;rmeliyiz.&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;[package]
name = "sys-trace"
version = "0.1.0"
description = "A simple system dashboard app written with Tauri"
authors = ["Burak Selim Şenyurt"]
edition = "2021"

[lib]
name = "sys_trace_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sysinfo = { version = "0.39.6", default-features = false, features = ["system", "disk"] }&lt;/pre&gt;
&lt;p&gt;`[lib]` b&amp;ouml;l&amp;uuml;m&amp;uuml;ne dikkat edelim. Tauri, &amp;ccedil;ekirdek kodu bir k&amp;uuml;t&amp;uuml;phane olarak paketler ve `main.rs` bunu sadece &amp;ccedil;ağırır. B&amp;ouml;ylelikle aynı kod tabanı mobil hedefler i&amp;ccedil;in de kullanılabilir hale gelir.&lt;/p&gt;
&lt;h2&gt;Rust Tarafındaki Veri Modeli&lt;/h2&gt;
&lt;p&gt;Şimdi sıra geldi asıl işi yapan kodları yazmaya. Ben Rust tarafını &amp;uuml;&amp;ccedil; par&amp;ccedil;aya b&amp;ouml;ld&amp;uuml;m. &amp;Ouml;ny&amp;uuml;ze taşıyacağımız veri modelleri i&amp;ccedil;in `metrics.rs`, &amp;ouml;l&amp;ccedil;&amp;uuml;mleri toplayan fonksiyonlar i&amp;ccedil;in `engine.rs` ve Tauri ile buluştuğumuz nokta olan `lib.rs`.&lt;/p&gt;
&lt;p&gt;İşe olmazsa olmaz veri modelleriyle başlayalım. `src-tauri/src/metrics.rs` dosyasını aşağıdaki gibi oluşturabiliriz.(Rust dilinde geliştirme yapıyor olsak bile program varlıklarını temsil eden veri modelleri ile &amp;ccedil;alışmak her zaman iyidir)&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use serde::Serialize;

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CpuCore {
    pub name: String,
    pub frequency: u64,
    pub usage: f32,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MemorySnapshot {
    pub total: u64,
    pub used: u64,
    pub available: u64,
    pub swap_total: u64,
    pub swap_used: u64,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DiskSnapshot {
    pub name: String,
    pub mount_point: String,
    pub file_system: String,
    pub kind: String,
    pub total_space: u64,
    pub available_space: u64,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Metrics {
    pub global_cpu: f32,
    pub cpu_cores: Vec&amp;lt;CpuCore&amp;gt;,
    pub memory: MemorySnapshot,
    pub disks: Vec&amp;lt;DiskSnapshot&amp;gt;,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HostInfo {
    pub name: String,
    pub os: String,
    pub kernel_version: String,
    pub architecture: String,
    pub cpu_brand: String,
    pub physical_cores: Option&amp;lt;usize&amp;gt;,
    pub logical_cores: usize,
}&lt;/pre&gt;
&lt;p&gt;Dosya boyunca tekrar eden iki direktif var. `Serialize`, bu veri yapılarının JSON'a d&amp;ouml;n&amp;uuml;şt&amp;uuml;r&amp;uuml;lebilmesini sağlıyor ki IPC hattından ge&amp;ccedil;ebilmesi i&amp;ccedil;in bu şart diyebilirim. `#[serde(rename_all = "camelCase")]` ile belirtilen rename_all değeri ise daha &amp;ouml;nemli. Rust tarafında `snake_case` yazım tarzını se&amp;ccedil;iyoruz ve `swap_total` gibi alan adları kullanıyoruz. Typescript tarafında ise `camelCase` daha yaygın bir isimlendirme standardı. Bu direktif sayesinde serileştirme sırasında `swap_total` alanı JSON'a `swapTotal` olarak yazılıyor. Yani iki d&amp;uuml;nyanın da kendi geleneğine sadık kalabiliyoruz(Biz ayrı d&amp;uuml;nyaların insanıyız demeye son) Bu arada `physical_cores` alanını `Option&amp;lt;usize&amp;gt;` olarak tanımladığımı fark etmiş olmalısınız. Zira sysinfo, her platformda fiziksel &amp;ccedil;ekirdek sayısını tespit edemeyebilir. Bu bilgi JSON tarafında `null` olarak yer alacak, &amp;ouml;ny&amp;uuml;zde de ona g&amp;ouml;re bir kontrol yapacağız.&lt;/p&gt;
&lt;h2&gt;&amp;Ouml;l&amp;ccedil;&amp;uuml;mleri Toplamak&lt;/h2&gt;
&lt;p&gt;Veri yapıları hazır olduğuna g&amp;ouml;re bunları dolduracak fonksiyonları geliştirmeye başlayabiliriz. Bu ama&amp;ccedil;la `src-tauri/src/engine.rs` isimli bir dosya oluşturup i&amp;ccedil;eriğini aşağıdaki gibi dolduralım.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;use crate::metrics::HostInfo;
use crate::metrics::Metrics;
use sysinfo::{Disks, System};

pub fn get_host_info(sys: &amp;amp;System) -&amp;gt; HostInfo {
    let name = System::host_name().unwrap_or_else(|| "Unknown".to_string());
    let os = System::long_os_version().unwrap_or_else(|| "Unknown".to_string());
    let kernel_version = System::kernel_version().unwrap_or_else(|| "Unknown".to_string());
    let architecture = std::env::consts::ARCH.to_string();
    let cpu_brand = sys
        .cpus()
        .first()
        .map_or("Unknown".to_string(), |cpu| cpu.brand().to_string());
    let physical_cores = System::physical_core_count();
    let logical_cores = sys.cpus().len();

    HostInfo {
        name,
        os,
        kernel_version,
        architecture,
        cpu_brand,
        physical_cores,
        logical_cores,
    }
}&lt;/pre&gt;
&lt;p&gt;`get_host_info` fonksiyonu makine ile ilgili ve &amp;ccedil;alışma boyunca değişmeyecek bilgileri topluyor. Bu bilgilerin bir kısmına(host adı, işletim sistemi s&amp;uuml;r&amp;uuml;m&amp;uuml;, kernel s&amp;uuml;r&amp;uuml;m&amp;uuml; gibi) System t&amp;uuml;r&amp;uuml; &amp;uuml;zerinden statik fonksiyonlarla erişebiliyoruz. Hepsi `Option&amp;lt;String&amp;gt;` d&amp;ouml;nd&amp;uuml;rd&amp;uuml;ğ&amp;uuml; i&amp;ccedil;in `unwrap_or_else` ile makul bir varsayılan değer veriyoruz. Mimari bilgisini ise sysinfo'ya sormaya gerek yok, `std::env::consts::ARCH` sabiti derleme zamanında zaten hedef mimariyi biliyor. Asıl iş y&amp;uuml;k&amp;uuml; ise s&amp;uuml;rekli &amp;ccedil;ağıracağımız `collect` fonksiyonunda. Bu fonksiyonu da aynı dosyaya ekleyerek devam edelim.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;pub fn collect(sys: &amp;amp;mut System, disks: &amp;amp;mut Disks) -&amp;gt; Metrics {
    sys.refresh_cpu_usage();
    sys.refresh_cpu_frequency();
    sys.refresh_memory();
    disks.refresh(true);

    let cpu_cores = sys
        .cpus()
        .iter()
        .map(|cpu| crate::metrics::CpuCore {
            name: cpu.name().to_string(),
            frequency: cpu.frequency(),
            usage: cpu.cpu_usage(),
        })
        .collect();

    let memory = crate::metrics::MemorySnapshot {
        total: sys.total_memory(),
        used: sys.used_memory(),
        available: sys.available_memory(),
        swap_total: sys.total_swap(),
        swap_used: sys.used_swap(),
    };

    let disks = disks
        .iter()
        .map(|disk| crate::metrics::DiskSnapshot {
            name: disk.name().to_string_lossy().to_string(),
            mount_point: disk.mount_point().to_string_lossy().to_string(),
            file_system: disk.file_system().to_string_lossy().to_string(),
            kind: format!("{:?}", disk.kind()),
            total_space: disk.total_space(),
            available_space: disk.available_space(),
        })
        .collect();

    crate::metrics::Metrics {
        global_cpu: sys.global_cpu_usage(),
        cpu_cores,
        memory,
        disks,
    }
}&lt;/pre&gt;
&lt;p&gt;Fonksiyonun ilk d&amp;ouml;rt satırı konuşmaya değer. sysinfo tembel(lazy) modda &amp;ccedil;alışır. Yani `sys.total_memory()` &amp;ccedil;ağrısı ger&amp;ccedil;ek zamanlı bir &amp;ouml;l&amp;ccedil;&amp;uuml;m yapmaz bunun yerine en son `refresh` sırasında alınan değeri d&amp;ouml;nd&amp;uuml;r&amp;uuml;r. Dolayısıyla her &amp;ouml;l&amp;ccedil;&amp;uuml;m turunda ilgili bileşenleri a&amp;ccedil;ık&amp;ccedil;a tazelememiz gerekiyor. Burada da yine sadece ihtiyacımız olanları yeniletiyoruz. S&amp;ouml;z gelimi `refresh_all` gibi kapsamlı bir &amp;ccedil;ağrı yapsaydık process listesi de dahil pek &amp;ccedil;ok gereksiz bilgi toplanacaktı ki saniyede bir &amp;ccedil;alışan bir d&amp;ouml;ng&amp;uuml;de bunun maliyeti hi&amp;ccedil; hoş olmaz.&lt;/p&gt;
&lt;p&gt;`disks.refresh(true)` satırındaki `true` değeri ise listede artık g&amp;ouml;r&amp;uuml;nmeyen diskleri koleksiyondan &amp;ccedil;ıkarmak anlamına geliyor. Uygulama &amp;ccedil;alışırken bir harici bellek takıp &amp;ccedil;ıkardığımızda listenin doğru g&amp;uuml;ncellenmesini istiyorsak bu parametre işimize yarayacaktır.&lt;/p&gt;
&lt;p&gt;K&amp;uuml;&amp;ccedil;&amp;uuml;k bir uyarı da CPU kullanımı hakkında. sysinfo, CPU kullanım y&amp;uuml;zdesini iki &amp;ouml;l&amp;ccedil;&amp;uuml;m arasındaki farktan hesaplar. Bu nedenle programın ilk &amp;ouml;l&amp;ccedil;&amp;uuml;m&amp;uuml;nde &amp;ccedil;ekirdek değerlerinin sıfır &amp;ccedil;ıkması son derece normaldir. İkinci turdan itibaren ger&amp;ccedil;ek değerler g&amp;ouml;r&amp;uuml;nmeye başlar. &amp;Ouml;rneğimizde tazeleme aralığı bir saniye olduğu i&amp;ccedil;in k&amp;uuml;t&amp;uuml;phanenin beklediği minimum aralığın da epeyce &amp;uuml;zerindeyiz. Ben sorun yaşamadım sizin de yaşayacağını d&amp;uuml;ş&amp;uuml;nm&amp;uuml;yorum.&lt;/p&gt;
&lt;p&gt;Son olarak `kind` alanı i&amp;ccedil;in kullandığımız `format!("{:?}", disk.kind())` ifadesine değinelim. sysinfo, disk t&amp;uuml;r&amp;uuml;n&amp;uuml; `DiskKind` isimli bir enum ile veriyor(HDD, SSD, Unknown)*. Bunu doğrudan serileştirmek yerine `Debug` &amp;ccedil;ıktısını metinsel bilgiye &amp;ccedil;eviriyoruz. Bu tamamen benim tembelliğimden kaynaklanıyor :D Daha temiz bir yaklaşım olarak `DiskKind` i&amp;ccedil;in kendi d&amp;ouml;n&amp;uuml;ş&amp;uuml;m fonksiyonumuzu yazabilirdik de.&lt;/p&gt;
&lt;h2&gt;Uygulama Durumu ve Command'ler&lt;/h2&gt;
&lt;p&gt;Şimdi geldik Rust tarafının Tauri ile buluştuğu yere. `src-tauri/src/lib.rs` dosyasında &amp;ouml;nce uygulama durumunu tutacak yapıyı tanımlayalım.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;mod engine;
mod metrics;

use std::sync::Mutex;
use sysinfo::{CpuRefreshKind, Disks, MemoryRefreshKind, RefreshKind, System};

use crate::metrics::Metrics;

/*
    Uygulamamız &amp;ccedil;alışırken System ve Disk bilgilerinin sık sık g&amp;uuml;ncellenmesi gerekecek ancak bu iş
    &amp;ccedil;apraz &amp;ccedil;ağrılarda eş zamanlılık sorunlarına yol a&amp;ccedil;abilir.
    Bu nedenle System ve Disk nesnelerini Mutex ile sarmalayarak eş zamanlı erişimi g&amp;uuml;venli hale getiriyoruz.
    Mutex, birden fazla iş par&amp;ccedil;acığının aynı anda System ve Disk nesnelerine erişmesini engeller
    ve sadece bir iş par&amp;ccedil;acığına erişim izni verir.

    Command ile işaretlenmiş fonksiyonlar worker thread pool &amp;uuml;zerinden &amp;ccedil;ağırıldığı i&amp;ccedil;in de
    bu Mutex yapısı, System ve Disk nesnelerine erişimde g&amp;uuml;venliği sağlar.
*/
pub struct AppState {
    pub system: Mutex&amp;lt;System&amp;gt;,
    pub disks: Mutex&amp;lt;Disks&amp;gt;,
}

impl AppState {
    fn new() -&amp;gt; Self {
        /*
            Sadece CPU ve bellek bilgilerini izleyeceğiz (track). Diğer sistem bilgilerine ihtiyacımız yok.
            Bu nedenle RefreshKind ile sadece gerekli alanları g&amp;uuml;ncelleyecek şekilde bir yapılandırma yapıyoruz.
        */
        let refresh = RefreshKind::nothing()
            .with_cpu(CpuRefreshKind::everything())
            .with_memory(MemoryRefreshKind::everything());

        Self {
            system: Mutex::new(System::new_with_specifics(refresh)),
            disks: Mutex::new(Disks::new_with_refreshed_list()),
        }
    }
}&lt;/pre&gt;
&lt;p&gt;Burada &amp;uuml;zerinde durulması gereken asıl konu Mutex kullanımı. Neden ihtiya&amp;ccedil; duyduğumuz a&amp;ccedil;ıklamaya &amp;ccedil;alışayım. `System` nesnesini her &amp;ouml;l&amp;ccedil;&amp;uuml;mde yeniden oluşturmak hem maliyetli hem de anlamsız. Zira CPU kullanım y&amp;uuml;zdesinin hesaplanabilmesi i&amp;ccedil;in bir &amp;ouml;nceki &amp;ouml;l&amp;ccedil;&amp;uuml;m&amp;uuml;n hafızada tutuluyor olması gerekiyor. Dolayısıyla bu nesneyi uygulama &amp;ouml;mr&amp;uuml; boyunca yaşatmamız lazım. Tauri bunun i&amp;ccedil;in `manage` fonksiyonuyla kullanabileceğimiz bir durum(state) mekanizması sunuyor. Ancak command olarak işaretlediğimiz fonksiyonlar bir worker thread havuzu &amp;uuml;zerinden &amp;ccedil;ağırıldığından, aynı `System` nesnesine birden fazla iş par&amp;ccedil;acığından erişim ihtimali doğuyor. `Mutex` ile sarmalayarak bu erişimi g&amp;uuml;venli hale getiriyoruz. Rust'ın bize burada bir kolaylık sağladığını da s&amp;ouml;ylemeliyim zira `Mutex` kullanmasak zaten uygulama kodumuz b&amp;uuml;y&amp;uuml;k ihtimalle derlenmeyecekti.(Fakat siz bana g&amp;uuml;venmeyin ve bunu kendiniz deneyip ispatlayın) `RefreshKind::nothing()` ile başlayıp sadece CPU ve bellek bileşenlerini eklememiz ise `collect` fonksiyonundaki yaklaşımın bir devamı niteliğinde. Bir başka deyişle nesneyi baştan gereksiz bilgi toplamayacak şekilde yapılandırıyoruz.&lt;/p&gt;
&lt;p&gt;Sırada &amp;ouml;ny&amp;uuml;z&amp;uuml;n &amp;ccedil;ağıracağı fonksiyonlar var.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;#[tauri::command]
fn get_metrics(state: tauri::State&amp;lt;'_, AppState&amp;gt;) -&amp;gt; Result&amp;lt;Metrics, String&amp;gt; {
    let mut system = state
        .system
        .lock()
        .map_err(|_| " Sys state poisoned".to_string())?;
    let mut disks = state
        .disks
        .lock()
        .map_err(|_| " Disk state poisoned".to_string())?;
    Ok(engine::collect(&amp;amp;mut system, &amp;amp;mut disks))
}

#[tauri::command]
fn get_host_info(state: tauri::State&amp;lt;'_, AppState&amp;gt;) -&amp;gt; Result&amp;lt;metrics::HostInfo, String&amp;gt; {
    let system = state
        .system
        .lock()
        .map_err(|_| " Sys state poisoned".to_string())?;
    Ok(engine::get_host_info(&amp;amp;system))
}&lt;/pre&gt;
&lt;p&gt;`#[tauri::command]` direktifi ile işaretlenen fonksiyonlar &amp;ouml;ny&amp;uuml;zden `invoke` ile &amp;ccedil;ağırılabilir hale geliyor. Fonksiyon imzasındaki `tauri::State&amp;lt;'_, AppState&amp;gt;` parametresini biz doldurmuyoruz, Tauri bunu &amp;ccedil;alışma zamanında otomatik olarak enjekte ediyor.(Bir nevi dependency injection ger&amp;ccedil;ekleştiriyoruz diyebilirim)*&lt;/p&gt;
&lt;p&gt;D&amp;ouml;n&amp;uuml;ş t&amp;uuml;r&amp;uuml; olarak `Result&amp;lt;T, String&amp;gt;` se&amp;ccedil;ilmesinin de bir sebebi var. Bir command `Result` d&amp;ouml;nd&amp;uuml;rd&amp;uuml;ğ&amp;uuml;nde Tauri, `Err` durumunu &amp;ouml;ny&amp;uuml;z tarafında bir Promise rejection'a &amp;ccedil;eviriyor. B&amp;ouml;ylece Typescript tarafında `try/catch` ile hatayı yakalayabiliyoruz. Hata t&amp;uuml;r&amp;uuml; olarak `String` kullanmamızın sebebi serileşebilir olma zorunluluğu. Daha kurumsal bir &amp;ccedil;alışmada `thiserror` gibi bir k&amp;uuml;feyle kendi hata t&amp;uuml;r&amp;uuml;m&amp;uuml;z&amp;uuml; tanımlayıp `Serialize` implementasyonunu vermek &amp;ccedil;ok daha şık olacaktır.&lt;/p&gt;
&lt;p&gt;Diğer yandan `lock()` &amp;ccedil;ağrısındaki `map_err` kısmına bakmakta da yarar var. Rust'ta bir `Mutex`, kilidi tutan iş par&amp;ccedil;acığı panic fırlatırsa zehirlenebilir(poisoned duruma ge&amp;ccedil;er) Bu durumda `lock()` &amp;ccedil;ağrısı da hata d&amp;ouml;ner. Uygulamamızın kilitlenmesi yerine &amp;ouml;ny&amp;uuml;ze anlamlı bir mesaj g&amp;ouml;ndermeyi tercih ediyoruz.&lt;/p&gt;
&lt;p&gt;Nihayetinde uygulamayı ayağa kaldıran `run` fonksiyonunu yazarak devam edelim.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .manage(AppState::new())
        .invoke_handler(tauri::generate_handler![get_metrics, get_host_info])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}&lt;/pre&gt;
&lt;p&gt;`manage` ile oluşturduğumuz `AppState` &amp;ouml;rneğini Tauri'nin state havuzuna bırakıyoruz. `generate_handler!` makrosu ise &amp;ouml;ny&amp;uuml;zden &amp;ccedil;ağırılabilecek fonksiyonların listesini derleme zamanında oluşturuyor. Buraya eklemeyi unuttuğumuz bir fonksiyonu `invoke` ile &amp;ccedil;ağırmaya kalkarsak &amp;ccedil;alışma zamanında hata alırız ki bu benim de birka&amp;ccedil; kez d&amp;uuml;şt&amp;uuml;ğ&amp;uuml;m bir tuzak oldu :D&lt;/p&gt;
&lt;p&gt;`main.rs` dosyasını değiştirmemize gerek yok. Scaffolding sırasında gelen haliyle bırakabiliriz.&lt;/p&gt;
&lt;pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false"&gt;// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
    sys_trace_lib::run()
}&lt;/pre&gt;
&lt;p&gt;`windows_subsystem = "windows"` &amp;ouml;zelliği release derlemesinde uygulamanın arkasında bir konsol penceresi a&amp;ccedil;ılmasını engeller. Yorum satırındaki uyarıyı ciddiye almakta fayda var.&lt;/p&gt;
&lt;h2&gt;Konfig&amp;uuml;rasyon ve İzinler&lt;/h2&gt;
&lt;p&gt;Rust tarafı bitti ancak &amp;ouml;ny&amp;uuml;ze ge&amp;ccedil;meden &amp;ouml;nce iki konfig&amp;uuml;rasyon dosyasına bakmamız lazım. İlki `src-tauri/capabilities/default.json` ve aşağıdaki i&amp;ccedil;eriğe sahip.&lt;/p&gt;
&lt;pre class="brush:xml;auto-links:false;toolbar:false" contenteditable="false"&gt;{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "opener:default"
  ]
}&lt;/pre&gt;
&lt;p&gt;Tauri 2 ile gelen izin modeli framework'&amp;uuml;n &amp;ouml;nemli &amp;ouml;zelliklerinden birisi. Uygulamanın hangi pencerede hangi yetenekleri kullanabileceğini a&amp;ccedil;ık&amp;ccedil;a beyan etmek gerekiyor. Makaledeki &amp;ouml;rneğimizde ekstra bir izne ihtiya&amp;ccedil; yok &amp;ccedil;&amp;uuml;nk&amp;uuml; sistem bilgilerini bir plugin &amp;uuml;zerinden değil kendi yazdığımız command bileşenlerinden alıyoruz. Ancak dosya sistemine erişmemiz veya bir shell komutu &amp;ccedil;alıştırmamız gerekseydi ilgili izinleri buraya eklememiz gerekecekti. Tahmin edeceğiniz &amp;uuml;zere permissions tarafı olduk&amp;ccedil;a kritik.&lt;/p&gt;
&lt;p&gt;İkinci konfigurasyon dosyası ise `src-tauri/tauri.conf.json`. Pencere boyutları, tema ve g&amp;uuml;venlik ayarları burada yapılıyor.&lt;/p&gt;
&lt;pre class="brush:xml;auto-links:false;toolbar:false" contenteditable="false"&gt;{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "sys-trace",
  "version": "0.1.0",
  "identifier": "com.buraks.sys-trace",
  "build": {
    "beforeDevCommand": "npm run dev",
    "devUrl": "http://localhost:1420",
    "beforeBuildCommand": "npm run build",
    "frontendDist": "../dist"
  },
  "app": {
    "withGlobalTauri": true,
    "windows": [
      {
        "title": "sys-trace",
        "width": 800,
        "height": 720,
        "minWidth": 560,
        "minHeight": 480,
        "resizable": true,
        "theme": "Dark"
      }
    ],
    "security": {
      "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'"
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ]
  }
}&lt;/pre&gt;
&lt;p&gt;Ben &amp;ccedil;ok fazla şey değiştirmedim ama g&amp;ouml;rd&amp;uuml;ğ&amp;uuml;n&amp;uuml;z gibi bundle paketine eklenecek simgelerden, build aşamasındaki detaylara kadar birka&amp;ccedil; şey var. `csp` ayarı ile WebView'ın sadece kendi kaynaklarına erişmesine izin veriyoruz. `style-src` i&amp;ccedil;in `unsafe-inline` eklenmiş durumda &amp;ccedil;&amp;uuml;nk&amp;uuml; &amp;ouml;rnekte inline stiller kullanıyoruz. &amp;Uuml;retim ortamına &amp;ccedil;ıkacak bir uygulamada bunu kaldırıp stilleri ayrı bir dosyaya taşımak daha doğru olabilir elbette. `build` b&amp;ouml;l&amp;uuml;m&amp;uuml;nde ise Tauri'nin geliştirme sırasında Vite sunucusunu nasıl ayağa kaldıracağı ve paketleme sırasında hangi klas&amp;ouml;rdeki &amp;ccedil;ıktıyı kullanacağı belirtiliyor.&lt;/p&gt;
&lt;h2&gt;&amp;Ouml;ny&amp;uuml;z Tarafı&lt;/h2&gt;
&lt;p&gt;Aray&amp;uuml;z i&amp;ccedil;in Material Web bileşenlerini tercih ettim. Kullanabilmek i&amp;ccedil;in &amp;ouml;nce ilgili paketi npm ile y&amp;uuml;klemek gerekiyor. Ben npm dedim ama kuruluma g&amp;ouml;re yarn veya farklı bir alternatif de tercih edilebilir.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;npm install @material/web&lt;/pre&gt;
&lt;p&gt;Ardından `index.html` i&amp;ccedil;eriğini aşağıdaki gibi d&amp;uuml;zenleyebiliriz. Burada esas olarak bir grid kullanılıyor ve verilerin yerleşeceği alanları `id` ile işaretliyoruz. Hemen &amp;ouml;nemli bir notu vurgulayalım. Ezelden beri tasarım konusunda beceriksizimdir ve genelde &amp;ccedil;ok zorlanırım. Aşağıdaki aray&amp;uuml;z tasarımını tamamen GPT 5.6 SOL'a yaptırdım. Sadece Rust ile olan ilişkileri kurguladım diyebilirim.&lt;/p&gt;
&lt;pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false"&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="en"&amp;gt;

&amp;lt;head&amp;gt;
  &amp;lt;meta charset="UTF-8" /&amp;gt;
  &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&amp;gt;
  &amp;lt;title&amp;gt;System Dashboard&amp;lt;/title&amp;gt;
  &amp;lt;script type="module" src="/src/main.ts" defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
  &amp;lt;main style="max-width: 1280px; margin: 0 auto; padding: 8px;"&amp;gt;
    &amp;lt;div style="display:grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: repeat(4, auto); gap:8px;"&amp;gt;
      &amp;lt;md-elevated-card style="display:block; padding:10px; grid-column: 1 / span 2; grid-row: 1 / span 2;"&amp;gt;
        &amp;lt;h2 style="margin: 0;"&amp;gt;Summary&amp;lt;/h2&amp;gt;
        &amp;lt;h1 id="hostname" style="margin: 6px 0 4px 0;"&amp;gt;&amp;mdash;&amp;lt;/h1&amp;gt;
        &amp;lt;p id="hostmeta" style="margin: 0 0 8px 0;"&amp;gt;connecting&amp;hellip;&amp;lt;/p&amp;gt;
        &amp;lt;div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px;"&amp;gt;
          &amp;lt;md-outlined-card style="display:block; padding:8px;"&amp;gt;
            &amp;lt;div&amp;gt;CPU&amp;lt;/div&amp;gt;
            &amp;lt;div id="cpu-total"&amp;gt;&amp;mdash;&amp;lt;/div&amp;gt;
          &amp;lt;/md-outlined-card&amp;gt;
          &amp;lt;md-outlined-card style="display:block; padding:8px;"&amp;gt;
            &amp;lt;div&amp;gt;Memory&amp;lt;/div&amp;gt;
            &amp;lt;div id="mem-total"&amp;gt;&amp;mdash;&amp;lt;/div&amp;gt;
          &amp;lt;/md-outlined-card&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;p id="loadavg" style="margin: 8px 0 0 0;"&amp;gt;&amp;lt;/p&amp;gt;
      &amp;lt;/md-elevated-card&amp;gt;

      &amp;lt;md-elevated-card style="display:block; padding:10px; grid-column: 3 / span 2; grid-row: 1 / span 2;"&amp;gt;
        &amp;lt;h2 style="margin: 0 0 8px 0;"&amp;gt;CPU Cores&amp;lt;/h2&amp;gt;
        &amp;lt;div id="cores"&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/md-elevated-card&amp;gt;

      &amp;lt;md-outlined-card style="display:block; padding:10px; grid-column: 1 / span 4; grid-row: 3 / span 2;"&amp;gt;
        &amp;lt;h2 style="margin: 0 0 8px 0;"&amp;gt;Memory &amp;amp; Storage&amp;lt;/h2&amp;gt;
        &amp;lt;div style="display:grid; grid-template-columns: 1fr; gap:10px;"&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;h3 style="margin: 0 0 6px 0;"&amp;gt;Memory&amp;lt;/h3&amp;gt;
            &amp;lt;div id="memory"&amp;gt;&amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;h3 style="margin: 0 0 6px 0;"&amp;gt;Storage&amp;lt;/h3&amp;gt;
            &amp;lt;div id="disks"&amp;gt;&amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/md-outlined-card&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/main&amp;gt;
&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;&lt;/pre&gt;
&lt;p&gt;Şimdi `src/main.ts` dosyasına ge&amp;ccedil;elim. &amp;Ouml;nce Rust tarafındaki veri modellerinin Typescript karşılıklarını tanımlamalıyız. Bu ama&amp;ccedil;la i&amp;ccedil;eriği aşağıdaki gibi oluşturabiliriz.&lt;/p&gt;
&lt;pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false"&gt;import { invoke } from "@tauri-apps/api/core";
import "@material/web/all.js";

const REFRESH_MS = 1000;

type CpuCore = { name: string; usage: number; frequency: number };
type MemorySnapshot = {
  total: number; used: number; available: number;
  swapTotal: number; swapUsed: number;
};
type DiskSnapshot = {
  name: string; mountPoint: string; fileSystem: string; kind: string;
  totalSpace: number; availableSpace: number;
};
type Metrics = {
  globalCpu: number; cpuCores: CpuCore[]; memory: MemorySnapshot;
  disks: DiskSnapshot[];
};
type HostInfo = {
  name: string; os: string; kernelVersion: string; architecture: string;
  cpuBrand: string; physicalCores: number | null; logicalCores: number;
};&lt;/pre&gt;
&lt;p&gt;Alan adlarının `camelCase` olduğuna dikkat edelim l&amp;uuml;tfen. Rust tarafında `#[serde(rename_all = "camelCase")]` direktifini kullanmamızın karşılığını burada alıyoruz. `physicalCores` alanının `number | null` olarak tanımlanması da Rust tarafındaki `Option&amp;lt;usize&amp;gt;` t&amp;uuml;r kullanımının bir karşılığı. Zira Typescript tarafına bu şekilde ifade edebiliriz.&lt;/p&gt;
&lt;p&gt;Tabii burada dikkat edilmesi gereken bir konu var. Bu t&amp;uuml;r tanımlarını elle yazdığımızda iki taraf arasında sessiz bir bağımlılık oluşturmaktayız. Rust tarafında bir alan adını değiştirdiğimizde Typescript tarafı bundan haberdar olmayabilir &amp;ccedil;&amp;uuml;nk&amp;uuml; değiştirmeyi unutabiliriz ve sorunu ancak &amp;ccedil;alışma zamanında alacağımız hata sonrası fark edebiliriz. ts-rs veya specta gibi k&amp;uuml;feler Rust t&amp;uuml;rlerinden otomatik olarak Typescript t&amp;uuml;r tanımları &amp;uuml;retebiliyorlar. Sonraki denemelerde bu k&amp;uuml;feleri ele almayı da d&amp;uuml;ş&amp;uuml;nebilirsiniz. Kodun devamında birka&amp;ccedil; yardımcı fonksiyonumuz bulunuyor. Bunlar veriyi ekranda g&amp;ouml;sterilebilir hale getirmek i&amp;ccedil;in kullandığımız par&amp;ccedil;alar. Her ikisi de GPT 5.6 SOL tarafından &amp;uuml;retildi. Tasarımı beğenmezseniz kabahat bende değil :D&lt;/p&gt;
&lt;pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false"&gt;const $ = (id: string) =&amp;gt; document.getElementById(id)!;

function gib(bytes: number): string {
  return (bytes / 1024  3).toFixed(1) + " GiB";
}

function band(pct: number): string {
  return pct &amp;lt; 50 ? "calm" : pct &amp;lt; 85 ? "warm" : "hot";
}

function meter(pct: number, label: string, detail: string): string {
  const clamped = Math.min(100, Math.max(0, pct));
  const state = band(clamped);
  const color = state === "calm" ? "#4caf50" : state === "warm" ? "#ff9800" : "#f44336";
  return `
    &amp;lt;div style="display:grid; grid-template-columns: minmax(120px, 1fr) minmax(130px, 1fr) auto; align-items:center; gap:6px; margin-bottom:4px;"&amp;gt;
      &amp;lt;div style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"&amp;gt;${label}&amp;lt;/div&amp;gt;
      &amp;lt;md-linear-progress value="${(clamped / 100).toFixed(3)}" style="--md-linear-progress-active-indicator-color:${color};"&amp;gt;&amp;lt;/md-linear-progress&amp;gt;
      &amp;lt;div style="font-variant-numeric: tabular-nums; white-space:nowrap; font-size:12px;"&amp;gt;${detail} &amp;middot; ${clamped.toFixed(1)}%&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;`;
}

function coreMeter(c: CpuCore): string {
  const clamped = Math.min(100, Math.max(0, c.usage));
  const state = band(clamped);
  const color = state === "calm" ? "#4caf50" : state === "warm" ? "#ff9800" : "#f44336";
  return `
    &amp;lt;div title="${c.name} &amp;middot; ${c.usage.toFixed(1)}% @ ${c.frequency} MHz" style="display:flex; flex-direction:column; align-items:center; width:20px;"&amp;gt;
      &amp;lt;div style="height:92px; width:14px; border-radius:6px; background:#e0e0e0; display:flex; align-items:flex-end; overflow:hidden;"&amp;gt;
        &amp;lt;div style="width:100%; height:${clamped}%; background:${color};"&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div style="font-size:11px; margin-top:3px;"&amp;gt;${c.name.replace(/^cpu/i, "").trim() || c.name}&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;`;
}&lt;/pre&gt;
&lt;p&gt;`gib` fonksiyonu Rust tarafından byte cinsinden gelen değerleri GiB'e &amp;ccedil;eviriyor. `band` fonksiyonu ise kullanım y&amp;uuml;zdesine g&amp;ouml;re bir renk bandı belirliyor. Buna g&amp;ouml;re y&amp;uuml;zde ellinin altı yeşil, seksen beşin altı turuncu ve &amp;uuml;zeri kırmızı renkte. `meter` yatay bir ilerleme &amp;ccedil;ubuğu &amp;uuml;retirken `coreMeter` her CPU &amp;ccedil;ekirdeği i&amp;ccedil;in dikey bir s&amp;uuml;tun &amp;uuml;retiyor.(Yani şu tasarımı sanıyorum birka&amp;ccedil; g&amp;uuml;n uğraşsam yapamazdım. Ey y&amp;uuml;ce EyAy sen neymişsin be abi!)*&lt;/p&gt;
&lt;p&gt;Şimdi de gelen veriyi ekrana basan fonksiyonlara bakalım.&lt;/p&gt;
&lt;pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false"&gt;function renderMessage(targetId: string, message: string): void {
  $(targetId).innerHTML = `&amp;lt;p&amp;gt;${message}&amp;lt;/p&amp;gt;`;
}

function renderHost(info: HostInfo) {
  $("hostname").textContent = info.name;
  const cores = info.physicalCores
    ? `${info.physicalCores} cores / ${info.logicalCores} threads`
    : `${info.logicalCores} threads`;
  $("hostmeta").textContent =
    `${info.cpuBrand} &amp;middot; ${cores} &amp;middot; ${info.os} &amp;middot; kernel ${info.kernelVersion} &amp;middot; ${info.architecture}`;
}

function render(m: Metrics) {
  $("cpu-total").textContent = m.globalCpu.toFixed(1) + "%";
  $("cores").innerHTML = `
    &amp;lt;div style="display:flex; align-items:flex-end; gap:6px; overflow-x:auto; padding-bottom:2px;"&amp;gt;
      ${m.cpuCores.map(coreMeter).join("")}
    &amp;lt;/div&amp;gt;`;
  $("loadavg").textContent = "";

  const memPct = (m.memory.used / m.memory.total) * 100;
  $("mem-total").textContent = `${gib(m.memory.used)} / ${gib(m.memory.total)}`;
  const swapPct = m.memory.swapTotal
    ? (m.memory.swapUsed / m.memory.swapTotal) * 100
    : 0;
  $("memory").innerHTML =
    meter(memPct, "RAM", `${gib(m.memory.used)} / ${gib(m.memory.total)} &amp;middot; ${gib(m.memory.available)} free`) +
    (m.memory.swapTotal &amp;gt; 0
      ? meter(swapPct, "swap", `${gib(m.memory.swapUsed)} / ${gib(m.memory.swapTotal)}`)
      : "");

  $("disks").innerHTML = m.disks
    .map((d) =&amp;gt; {
      const used = d.totalSpace - d.availableSpace;
      const pct = (used / d.totalSpace) * 100;
      const tag = `${d.name} &amp;middot; ${d.mountPoint} &amp;middot; ${d.fileSystem} &amp;middot; ${d.kind}`;
      return meter(pct, tag, `${gib(used)} / ${gib(d.totalSpace)}`);
    })
    .join("");
}&lt;/pre&gt;
&lt;p&gt;`renderHost` fonksiyonunda `physicalCores` değerinin `null` gelebileceğinin hesaba katıldığını fark etmiş olmalısınız. Değer yoksa sadece mantıksal &amp;ccedil;ekirdek sayısı g&amp;ouml;steriliyor. `render` fonksiyonunda ise takas alanı(swap) i&amp;ccedil;in de bir kontrol var. Swap tanımlı değilse o satır hi&amp;ccedil; &amp;ccedil;izilmiyor. Nihayet en can alıcı kısma geldik. IPC hattının kullanıldığı yer...&lt;/p&gt;
&lt;pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false"&gt;async function tick() {
  try {
    render(await invoke&amp;lt;Metrics&amp;gt;("get_metrics"));
  } catch (err) {
    $("hostmeta").textContent = `sampling failed: ${err}`;
    renderMessage("cores", "Unable to fetch processor metrics.");
    renderMessage("memory", "Unable to fetch memory metrics.");
    renderMessage("disks", "Unable to fetch storage metrics.");
  }
  setTimeout(tick, REFRESH_MS);
}

async function start() {
  try {
    renderHost(await invoke&amp;lt;HostInfo&amp;gt;("get_host_info"));
  } catch (err) {
    $("hostmeta").textContent = `could not read host info: ${err}`;
  }
  tick();
}

start();&lt;/pre&gt;
&lt;p&gt;`invoke&amp;lt;Metrics&amp;gt;("get_metrics")` &amp;ccedil;ağrısındaki metin ifadesi, Rust tarafındaki fonksiyonun adıyla birebir aynı olmak zorunda. Bu da biraz &amp;ouml;nce değindiğimiz sessiz bağımlılığın bir başka &amp;ouml;rneği. Generic parametre olarak verdiğimiz `Metrics` t&amp;uuml;r&amp;uuml; ise sadece Typescript'in derleme zamanındaki bilgisi. Yani gelen JSON i&amp;ccedil;eriği ger&amp;ccedil;ekten &amp;ccedil;alışma zamanında doğrulanmıyor. Bu nedenle IPC hattından ne geldiğine dikkat etmemiz gerekiyor. D&amp;ouml;ng&amp;uuml; i&amp;ccedil;in `setInterval` yerine `setTimeout` kullanılıyor. B&amp;ouml;ylece bir sonraki &amp;ouml;l&amp;ccedil;&amp;uuml;m ancak mevcut &amp;ouml;l&amp;ccedil;&amp;uuml;m tamamlandıktan sonra planlanıyor. `setInterval` kullanmayı tercih etseydik &amp;ouml;l&amp;ccedil;&amp;uuml;m herhangi bir sebeple bir saniyeden uzun s&amp;uuml;rd&amp;uuml;ğ&amp;uuml;nde &amp;ccedil;ağrıların &amp;uuml;st &amp;uuml;ste binmesi s&amp;ouml;z konusu olabilirdi. Yine de bunu bir denemekte yarar var.&lt;/p&gt;
&lt;p&gt;`start` fonksiyonu değişmeyen host bilgilerini bir kez alıp ekrana yazıyor ve ardından d&amp;ouml;ng&amp;uuml;y&amp;uuml; başlatıyor. Buradaki hata yakalama bloğu da kayda değer. Rust tarafındaki command'ler `Result` d&amp;ouml;nd&amp;uuml;rd&amp;uuml;ğ&amp;uuml; i&amp;ccedil;in `Err` durumu buraya bir exception olarak d&amp;uuml;ş&amp;uuml;yor ve en azından kullanıcıya durumla ilgili bir mesaj g&amp;ouml;sterilebiliyor.&lt;/p&gt;
&lt;h2&gt;&amp;Ccedil;alışma Zamanı(Runtime)&lt;/h2&gt;
&lt;p&gt;Rust tarafındaki geliştirmeler ile &amp;ouml;ny&amp;uuml;z tarafı bittikten sonra &amp;ccedil;alışır bir &amp;ouml;rnekle karşılaşmış oldum. Sizde de benzer sonu&amp;ccedil;ların &amp;ccedil;ıkması gerekiyor.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Agustos/Runtime_01.png" alt="" /&gt;&lt;/p&gt;
&lt;h2&gt;Paketleme(Building)&lt;/h2&gt;
&lt;p&gt;Uygulama geliştirmelerini tamamladıktan sonra bir `deb` paketi oluşturabiliriz. Bunun i&amp;ccedil;in aşağıdaki komutu &amp;ccedil;alıştırmamız yeterli.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;npm run tauri build

# Yukarıdaki işlem sonrasında bundle/deb ve bundle/appImage klas&amp;ouml;rleri oluşacaktır.
# Bu klas&amp;ouml;rler altında paketlenmiş uygulamayı bulabiliriz.

# Kurulum da olduk&amp;ccedil;a basittir.
sudo dpkg -i src-tauri/target/release/bundle/deb/sys-trace_0.1.0_amd64.deb
sys-trace

# Ya da AppImage olarak &amp;ccedil;alıştırabiliriz.
chmod +x src-tauri/target/release/bundle/appimage/sys-trace-0.1.0_amd64.AppImage
./src-tauri/target/release/bundle/appimage/sys-trace-0.1.0_amd64.AppImage
&lt;/pre&gt;
&lt;p&gt;Aynı kod tabanını Windows tarafında da denedim. Rust ve Typescript kodlarında tek bir satır değiştirmeye gerek kalmadı. Yalnız `package.json` i&amp;ccedil;erisine platforma &amp;ouml;zel CLI paketinin(`@tauri-apps/cli-win32-x64-msvc`) eklenmesi gerekti. Paketleme komutu tamamen aynı. `npm run tauri build` &amp;ccedil;ağrısı bu sefer `deb` yerine `msi` ve `exe` &amp;ccedil;ıktıları &amp;uuml;retmeli. Tabii &amp;ccedil;apraz platform derleme yapmadığımızı, her platformda ayrı ayrı derlediğimizi hatırlatmam gerekiyor. Yani Windows kurulum paketi i&amp;ccedil;in bir Windows makineye ihtiyacımız var gibi.&lt;/p&gt;
&lt;h2&gt;Sonu&amp;ccedil;&lt;/h2&gt;
&lt;p&gt;Tauri ile ilk kez bir uygulama geliştirmiş oldum. Tasarım aşamasına kadar yani Rust tarafında kaldığım s&amp;uuml;rece epey keyifliydi. Yine de zamanın Delphi ve Windows Forms aray&amp;uuml;zlerindeki s&amp;uuml;r&amp;uuml;kle bırak rahatlığının olmadığını a&amp;ccedil;ık&amp;ccedil;a itiraf etmeliyim. Bileşenleri bir pencereye g&amp;ouml;rerek bırakmakla tasarımı HTML seviyesinde veya karman &amp;ccedil;orman CSS stilleri ile(her ne kadar modern framework'ler ile bu kolaylansa da) yapmak zorunda kalmak can sıkıcı. Bu macera ile ilgili sonu&amp;ccedil;ları da ş&amp;ouml;yle bırakalım &amp;ouml;yleyse.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sorumlulukların ayrımı &amp;ccedil;ok net(Seperation of Concerns olmuş diyebilir miyiz?)*. İşletim sistemine dokunan her şey Rust tarafında, g&amp;ouml;rsellik ise tamamen web d&amp;uuml;nyasında kalıyor. Bu ayrım &amp;ouml;ny&amp;uuml;z tarafında hangi teknolojiyi se&amp;ccedil;ersek se&amp;ccedil;elim arka planın değişmemesi anlamına da gelmekte ve pek tabii arka planda Rust dilinin g&amp;uuml;venli ve y&amp;uuml;ksek performans vaat eden yapısından faydalanabiliyoruz.&lt;/li&gt;
&lt;li&gt;İzin modeli(capabilities) uygulamanın neye erişebileceğini a&amp;ccedil;ık&amp;ccedil;a beyan etmemizi zorunlu kılıyor. Yazılımın g&amp;uuml;venliği a&amp;ccedil;ısından olduk&amp;ccedil;a değerli.&lt;/li&gt;
&lt;li&gt;İki d&amp;uuml;nya arasındaki t&amp;uuml;r eşleşmesini bu &amp;ouml;rnekte elle yaptık ve bu insan hatasına a&amp;ccedil;ık. Rust tarafındaki bir alan adı değişikliğinin &amp;ouml;ny&amp;uuml;z&amp;uuml; sessizce bozması pekala m&amp;uuml;mk&amp;uuml;n. &lt;a href="https://crates.io/crates/ts-rs" target="_blank"&gt;ts-rs&lt;/a&gt; veya &lt;a href="https://crates.io/crates/specta" target="_blank"&gt;specta&lt;/a&gt; gibi &amp;ccedil;&amp;ouml;z&amp;uuml;mlere bakmakta fayda var.&lt;/li&gt;
&lt;li&gt;Bu &amp;ouml;rnekte veriyi &amp;ouml;ny&amp;uuml;zden istiyoruz yani Command yaklaşımını kullandık. Halbuki metrikleri Rust tarafında bir arka plan thread'inde toplayıp Event ile yayınlamak daha yerinde bir tasarım olabilir ki bu y&amp;ouml;nde telkinler de var. B&amp;ouml;ylece g&amp;uuml;ncelleme sıklığını &amp;ouml;ny&amp;uuml;z&amp;uuml;n insafına bırakmamış oluruz.(Bunu bir sonraki denemeye not alıyorum.)*&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Elbette eksikler de var. Uygulamanın bir grafik ge&amp;ccedil;mişi yok bir başka deyişle CPU kullanımının son otuz saniyedeki seyrini g&amp;ouml;remiyoruz &amp;ouml;rneğin. System tray desteği eklemedik ki bu t&amp;uuml;r bir aracın arka planda &amp;ccedil;alışıp ihtiya&amp;ccedil; halinde a&amp;ccedil;ılması &amp;ccedil;ok daha mantıklı olacaktır. Network arabirimleri ve process listesi de tamamen kapsam dışında bırakıldı. Bunları tek tek ele almak yerine &amp;ouml;ncelikle framework'&amp;uuml; tanımaya odaklandığımız bir &amp;ouml;rnek &amp;uuml;zerinde &amp;ccedil;alıştık diyebilirim. Belki ilerleyen zamanlarda bu eksikleri de tamamlarız.&lt;/p&gt;
&lt;p&gt;B&amp;ouml;ylece geldik bir &amp;ccedil;alışmamızın daha sonuna. Tekrardan g&amp;ouml;r&amp;uuml;ş&amp;uuml;nceye dek hepinize mutlu g&amp;uuml;nler dilerim.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/buraksenyurt/rust-farm/tree/main/handson/sys-trace" target="_blank"&gt;&amp;Ouml;rnek kodlara github &amp;uuml;zerinden ulaşmak i&amp;ccedil;in tıklayınız.&lt;/a&gt;&lt;/p&gt;</summary>
    <published>2026-08-31T07:45:00+00:00</published>
    <link rel="related" href="https://www.buraksenyurt.com/post/tauri-ile-masaustu-uygulama-gelistirme#comment" />
    <category term="Rust" />
    <betag:tag>rust</betag:tag>
    <betag:tag>tauri</betag:tag>
    <betag:tag>desktop</betag:tag>
    <betag:tag>ubuntu</betag:tag>
    <betag:tag>linux</betag:tag>
    <betag:tag>typescript</betag:tag>
    <betag:tag>html</betag:tag>
    <betag:tag>material design</betag:tag>
    <dc:publisher>bsenyurt</dc:publisher>
    <dc:description>Mesleğe ilk başladığım yıllarda Web teknolojileri daha yeni yeni yaygınlaşmaya başlamıştı. Java ilk sürümünü yayınlamışken ben daha çok Delphi ile Windows platformunda masaüstü uygulamalar geliştirmekle meşguldüm. Web tabanlı geliştirmenin avantajları bir yana masaüstü uygulamalarda konuk olunan işletim sisteminin gücünden yararlanma, state tutma şansına sahip olabiliyorduk. Elbette zaman içerisinde neredeyse tüm iş modelleri web tabanlı çözümlere geçti. Web 1.0, 2.0, 3.0 derken Internet'in neredeyse tüm altyapısına sızan sayısız JavaScript Framework arasında geçiş yapar olduk. Özellikle cross-platform tabanlı sistemleri düşününce karşımıza farklı hibrit çözümler de çıktı...</dc:description>
    <pingback:server>https://www.buraksenyurt.com/pingback.axd</pingback:server>
    <pingback:target>https://www.buraksenyurt.com/post.aspx?id=47603d9d-a454-4b8a-96b1-5844f1825e63</pingback:target>
    <slash:comments>0</slash:comments>
    <trackback:ping>https://www.buraksenyurt.com/trackback.axd?id=47603d9d-a454-4b8a-96b1-5844f1825e63</trackback:ping>
    <wfw:comment>https://www.buraksenyurt.com/post/tauri-ile-masaustu-uygulama-gelistirme#comment</wfw:comment>
    <wfw:commentRss>https://www.buraksenyurt.com/syndication.axd?post=47603d9d-a454-4b8a-96b1-5844f1825e63</wfw:commentRss>
  </entry>
  <entry>
    <id>https://www.buraksenyurt.com/post/spring-boot-ile-basit-bir-ddd-projesi</id>
    <title>Spring Boot ile Basit Bir DDD Projesi</title>
    <updated>2026-06-20T09:47:00+00:00</updated>
    <link rel="self" href="https://www.buraksenyurt.com/post.aspx?id=6aeb67b9-88f8-4b2e-a104-b2c76526aa95" />
    <link href="https://www.buraksenyurt.com/post/spring-boot-ile-basit-bir-ddd-projesi" />
    <author>
      <name>bsenyurt</name>
    </author>
    <summary type="html">&lt;p&gt;Kendimi bildim bileli C# programlama dili ile geliştirme yapıyorum. Hobi ama&amp;ccedil;lı ilgilendiğim Rust, Zig ve OCaml dillerini saymazsak profesyonel iş hayatım .NET ekosistemi &amp;uuml;zerinde ge&amp;ccedil;ti. &amp;Ccedil;ok uzun yıllar &amp;ouml;nce ise Java programlama dilini &amp;ouml;ğrenmeye &amp;ccedil;alışmış ve bu vesile ile `Java ile 24 Kahve Molası` isimli bir seri yazmıştım. 2004 yılında yazdığım makaleleri derleyip bir dok&amp;uuml;man haline de getirmiştim ki &lt;a href="https://www.buraksenyurt.com/post/Java-ile-24-Kahve-Molasc4b1-Dokumanc4b1" target="_blank"&gt;buradan&lt;/a&gt; erişebilirsiniz. Tabii aradan yıllar yıllar ge&amp;ccedil;ti. Yazılım geliştirme d&amp;uuml;nyası s&amp;uuml;rekli değişti ama bazı temel kavramlar ve ilkeler hala aynı. Nesne y&amp;ouml;nelimli dil prensipleri, SOLID ilkeleri, tasarım kalıpları, yazılım mimarileri halen daha programlama dilleri ve platformlara referans oluyor. İş d&amp;uuml;nyası &amp;ccedil;&amp;ouml;z&amp;uuml;mleri ve kurumsal projeler bu felsefeler &amp;uuml;zerine inşa ediliyor. S&amp;ouml;z&amp;uuml; fazla uzatmadan bu yazıda nelerden bahsedeceğimize gelelim.&lt;/p&gt;
&lt;p&gt;Amacım Domain Driven Design&lt;em&gt;(DDD)&lt;/em&gt; yaklaşımını yine hafifsiklet bir senaryo ile olabildiğince basit bir şekilde ele almak&lt;em&gt;(CQRS, Event Sourcing, Message Bus gibi konuları sonraya bırakıyorum)&lt;/em&gt; ancak bu sefer Java programlama dilini ve Spring Boot Framework kullanacağım. Başlarken Java dilinde duayen olan arkadaşlarımın affına sığınıyorum zira bir C# geliştiricisi personasıyla olaylara yaklaşacağım. Dolayısıyla Java'nın ve Spring Boot'un bazı niş yeteneklerini atlayabilirim. Bu konuda yorumlarla bana ve okurlarımıza destek olabilirsiniz. &amp;Ouml;yleyse başlayalım.&lt;/p&gt;
&lt;h2&gt;Senaryo&lt;/h2&gt;
&lt;p&gt;Bilgisayar oyunu kiralayabildiğimiz bir sistem &amp;uuml;zerinde duracağız. Artık b&amp;ouml;yle bir devri yaşamıyoruz tabii ki ama zamanında Commodore 64 oyunları kiraladığımızı ve hatta bildiğiniz ya da bilmediğiniz doksanlık m&amp;uuml;zik kasetlerine oyunlar y&amp;uuml;klettiğimizi hatırlıyorum. &amp;Ouml;ğrenme ama&amp;ccedil;lı kullanabileceğimiz keyifli bir senaryo olabilir. Tabii Domain Driven Design denince işin i&amp;ccedil;erisine giren bazı &amp;ouml;nemli unsurlar var. Bunlardan birisi herkesin s&amp;ouml;z konusu &amp;ccedil;&amp;ouml;z&amp;uuml;m i&amp;ccedil;in kullandığı ortak dil. Denizcilik alanında bir şeyler yazıyorsak kullanacağımız jargon ile oyun geliştirme sahasındaki jargon tamamen farklı olacaktır. Bu jargon genellikle Ubiquitous Language olarak anılır ve program kodu i&amp;ccedil;erisindeki enstr&amp;uuml;manların isimlendirilmesinde hayati &amp;ouml;nem arz eder. Tabii bunu oluşturmak i&amp;ccedil;in aslında kullanıcı hikayelerini detaylıca dinlemek ve senaryolaştırmak gerekir. Biz &amp;ccedil;ok basit birka&amp;ccedil; c&amp;uuml;mleyi baz alalım.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Oyunların her biri kataloğumuzda birer isim&lt;em&gt;(Pacman, Super Mario,...)&lt;/em&gt; olarak varlığını s&amp;uuml;rd&amp;uuml;r&amp;uuml;r. Mağazamızda oyun kutularından n adet olabilir.&lt;/li&gt;
&lt;li&gt;Sistemde abonelerimiz vardır&lt;em&gt;(Bu demoda aboneleri sadece bir ID olarak tanımlayacağız)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Abonelerimiz oyun kiralayabilirler ancak bir kiralama s&amp;uuml;resi vardır. Kiralanan oyunlar belli bir s&amp;uuml;rede geri getirilir.&lt;/li&gt;
&lt;li&gt;Abone, kiraladığı oyunu geri getirdiğinde kiralama sona erer ve oyun tekrar mağazada kiralanabilir hale gelir. Ama gecikme olursa bu gecikme s&amp;uuml;resi kadar ekstra &amp;uuml;cret &amp;ouml;denir. &amp;Ouml;rneğin gecikilen g&amp;uuml;n başına 2.5 TL ceza &amp;ouml;demek gibi.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bu c&amp;uuml;mlelerden aslında belli başlı iş kuralları da ortaya &amp;ccedil;ıkar. &amp;Ouml;rneğin;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mağaza stoğunda olmayan bir oyun kiralanamaz.&lt;/li&gt;
&lt;li&gt;Oyun kiralandığında kullanılabilir oyun sayısı 1 azalır, geri getirildiğinde 1 artar.&lt;/li&gt;
&lt;li&gt;Bir kiralama iki kez geri getirilemez.&lt;/li&gt;
&lt;li&gt;Gecikme &amp;uuml;creti i&amp;ccedil;in şu form&amp;uuml;l ge&amp;ccedil;erlidir: `gecikme &amp;uuml;creti = gecikilen g&amp;uuml;n sayısı * 2.5 TL`&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Buradaki basit iş kuralları domain model i&amp;ccedil;erisinde yaşar ve asla atlanamaz&lt;em&gt;(No Bypass)&lt;/em&gt;. B&amp;ouml;ylece iş kurallarımızın her zaman ge&amp;ccedil;erli olmasını garanti edebiliriz. DDD yaklaşımında domain'i dış etkenlerden izole etmek &amp;ccedil;ok &amp;ouml;nemlidir. Bana g&amp;ouml;re domain modelimiz first citizen' dir.&lt;/p&gt;
&lt;h2&gt;DDD &amp;Ouml;zeti&lt;/h2&gt;
&lt;p&gt;DDD daha &amp;ccedil;ok iş modelini kodun tam merkezine yerleştirmeye odaklanmış bir yaklaşımdır. Bu sayede iş kuralları ve mantığı, programın her yerinde tutarlı bir şekilde uygulanabilir. Ayrıca framework, veritabanı ve benzeri altyapı detaylarını dışarıda tutarak iş modelinin bağımsız ve test edilebilir olmasını hedefler. Bu &amp;ouml;rnek &amp;ouml;zelinde ele alacağımız kavramları aşağıdaki tablo ile &amp;ouml;zetleyebiliriz...&lt;/p&gt;
&lt;table border="1"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kavram&lt;/th&gt;
&lt;th&gt;Ne anlama gelir?&lt;/th&gt;
&lt;th&gt;Bizim &amp;ouml;rneğimizde neye karşılık gelir?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ubiquitous Language&lt;/td&gt;
&lt;td&gt;T&amp;uuml;m ekip (Geliştiriciler, İş Analistleri, Test Uzmanları, vb.) tarafından paylaşılan ortak dil.&lt;/td&gt;
&lt;td&gt;Game, Rental, lateFee, returnOn vb&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value Object&lt;/td&gt;
&lt;td&gt;Kimliği olmayan, sadece değerleriyle tanımlanan nesneler. Değiştirilemezdirler. (Immutable)&lt;/td&gt;
&lt;td&gt;Money, GameId gibi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity&lt;/td&gt;
&lt;td&gt;Kimliği olan, yaşam d&amp;ouml;ng&amp;uuml;s&amp;uuml; boyunca değişebilen nesneler.&lt;/td&gt;
&lt;td&gt;Game, Rental gibi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate&lt;/td&gt;
&lt;td&gt;Birbirleriyle ilişkili Entity ve Value Object'lerin bir araya gelerek oluşturduğu tutarlı bir b&amp;uuml;t&amp;uuml;n.&lt;/td&gt;
&lt;td&gt;RentalAggregate, GameAggregate gibi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate Root&lt;/td&gt;
&lt;td&gt;Aggregate'in dış d&amp;uuml;nya ile olan tek giriş noktasıdır.&lt;/td&gt;
&lt;td&gt;RentalAggregate'in root'u Rental, GameAggregate'in root'u Game gibi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;Aggregate'lerin saklanması ve erişilmesi i&amp;ccedil;in kullanılan koleksiyon benzeri aggregate yapıları.&lt;/td&gt;
&lt;td&gt;RentalRepository, GameRepository gibi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain Service&lt;/td&gt;
&lt;td&gt;Birden fazla Entity veya Value Object'in etkileşimde bulunduğu karmaşık iş mantığını i&amp;ccedil;eren servisler.&lt;/td&gt;
&lt;td&gt;Bu &amp;ouml;rnekte ele almayacağız.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Service&lt;/td&gt;
&lt;td&gt;Bir transaction (Use Case) orkestrasyonunu y&amp;ouml;neten servis.&lt;/td&gt;
&lt;td&gt;RentGameService, GameCatalogService gibi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Proje Mimarisi&lt;/h2&gt;
&lt;p&gt;Projede olduk&amp;ccedil;a yalın d&amp;uuml;ş&amp;uuml;nerek hareket edebiliriz ve katmanlı&lt;em&gt;(layered)&lt;/em&gt; bir mimari modeli kullanabiliriz. Biraz ucundan da olsa hexagonal mimari ilkelerine g&amp;ouml;re inşa edebiliriz. Altın kural bağımlılıkların y&amp;ouml;n&amp;uuml;n&amp;uuml;n&lt;em&gt;(Dependency Direction)&lt;/em&gt; doğru uygulanması. &amp;Ouml;rneğin domain paketi sıfır framework bağımlılığına sahip olmalı ki domain modelindeki iş kurallarını &amp;ccedil;ok kısa s&amp;uuml;relerde hi&amp;ccedil;bir dış framework bağımlılığı olmadan değiştirebilelim veya test edebilelim. Bu sayede domain modelimizi daha esnek ve s&amp;uuml;rd&amp;uuml;r&amp;uuml;lebilir bir hale getirebiliriz. Mimari kurguyu kabaca aşağıdaki şekilde olduğu gibi &amp;ouml;zetleyebiliriz.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDBasic.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;K&amp;uuml;&amp;ccedil;&amp;uuml;k bir not:&lt;/strong&gt; Ben bu projeyi bir Linux platformunda&lt;em&gt;(Ubuntu 26.04)&lt;/em&gt; geliştiriyorum ancak aynı prensipler diğer platformlar i&amp;ccedil;in de ge&amp;ccedil;erlidir.&lt;/p&gt;
&lt;h2&gt;Proje İskeletinin Oluşturulması&lt;/h2&gt;
&lt;p&gt;Bir .NET geliştiricisi olarak normalde bir solution a&amp;ccedil;ıp projeleri tek tek i&amp;ccedil;erisinde oluştururuz. İhtiyacımız olan bağımlılıkları ise projelere g&amp;ouml;re ekleriz. Java d&amp;uuml;nyasında bu t&amp;uuml;r işlemler ve &amp;ouml;zellikle bağımlılıkların en ideal şekilde tesis edilmesi uzun zamandır Spring Framework gibi ara&amp;ccedil;larla etkili bir şekilde yapılmakta. Benzer şekilde hareket edeceğiz. Spring Boot proje oluşturma aracı olan &lt;a href="https://start.spring.io/" target="_blank"&gt;Spring Initializr&lt;/a&gt; adresini kullanarak uygulama iskeletini ve t&amp;uuml;m gereklilikleri hazırlayabiliriz. Ama aynı işlemleri curl komutu ile terminalden de yapabiliriz.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDSpringBootInit.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;&amp;Ouml;zellikle kendi sistemimizde hangi Java ve Maven versiyonları olduğuna dikkat etmekte yarar var. Zira versiyon parametleri uyumlu olmalı. Resimde g&amp;ouml;r&amp;uuml;len işlemleri curl komutu ile terminalden yapmak istersek de aşağıdaki gibi hareket edebiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;curl https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.5.15 \
  -d javaVersion=25 \
  -d groupId=com.example \
  -d artifactId=game-rental \
  -d name=game-rental \
  -d packageName=com.example.gamerental \
  -d dependencies=web,data-jpa,postgresql,validation,flyway \
  -o game-rental.zip

unzip game-rental.zip -d game-rental
cd game-rental&lt;/pre&gt;
&lt;p&gt;Dikkat edileceği &amp;uuml;zere proje oluşturulurken bazı bağımlılıklar (dependencies) da ekledik. Hangisini ne i&amp;ccedil;in kullandığımızı &amp;ouml;zetleyelim.&lt;/p&gt;
&lt;table border="1"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Paket&lt;/th&gt;
&lt;th&gt;Ne işe yarar?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spring Web&lt;/td&gt;
&lt;td&gt;REST API geliştirmek i&amp;ccedil;in gerekli olan Spring MVC altyapısını sağlar. Tomcat gibi g&amp;ouml;m&amp;uuml;l&amp;uuml; bir web sunucusu i&amp;ccedil;erir.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data JPA&lt;/td&gt;
&lt;td&gt;ORM (Object-Relational Mapping) işlemleri i&amp;ccedil;in gerekli altyapıyı sağlar. JPA (Java Persistence API) standartlarını kullanarak veritabanı işlemlerini kolaylaştırır.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL Driver&lt;/td&gt;
&lt;td&gt;PostgreSQL veritabanına bağlanmak i&amp;ccedil;in gerekli s&amp;uuml;r&amp;uuml;c&amp;uuml;y&amp;uuml; sağlar.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Validation&lt;/td&gt;
&lt;td&gt;Bean Validation API'sini kullanarak veri doğrulama işlemlerini kolaylaştırır.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flyway&lt;/td&gt;
&lt;td&gt;Veritabanı şemasını y&amp;ouml;netmek ve s&amp;uuml;r&amp;uuml;m kontrol&amp;uuml; sağlamak i&amp;ccedil;in kullanılan bir ara&amp;ccedil;tır. Migrations işlemlerini kolaylaştırır.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Proje Oluşturulduktan Sonra Bazı Ayarlamalar&lt;/h2&gt;
&lt;p&gt;Proje oluşturulduktan sonra PostgreSQL desteği i&amp;ccedil;in yeni bir flyway bağımlılığı (dependency) eklememiz de gerekebilir. Bunun i&amp;ccedil;in t&amp;uuml;m proje ayarlarını i&amp;ccedil;eren `pom.xml` dosyasına aşağıdaki bağımlılığı eklemek yeterli olacaktır. Daha &amp;ouml;nceden eklediğimiz bağımlılıklar da bu dosyada yer alır.&lt;/p&gt;
&lt;pre class="brush:xml;auto-links:false;toolbar:false" contenteditable="false"&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.flywaydb&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;flyway-database-postgresql&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;x&lt;/pre&gt;
&lt;p&gt;Eğer projeyi komut satırından oluşturduysak konfigurasyon dosyası&lt;em&gt;(application.properties)&lt;/em&gt; YAML formatında oluşmamış olabilir. Bu dosyayı silip `application.yml` isimli yeni bir dosya oluşturup i&amp;ccedil;eriğini aşağıdaki şekilde d&amp;uuml;zenleyebiliriz.&lt;em&gt;(Dosya `src/main/resources` dizininde olmalıdır)&lt;/em&gt; Bu şart değil ancak yml formatında daha okunabilir ve hiyerarşik bir yapı sağladığı i&amp;ccedil;in tercih ediyorum. Tabii ki bu tamamen kişisel bir tercih.&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/gamerental
    username: gamerental
    password: secret

  jpa:
    hibernate:
      ddl-auto: validate
    open-in-view: false
    properties:
      hibernate:
        format_sql: true

  flyway:
    enabled: true

logging:
  level:
    org.hibernate.SQL: debug&lt;/pre&gt;
&lt;p&gt;Dikkat edileceği &amp;uuml;zere veritabanı bağlantı bilgisi, hibernate ve flyway ayarları yapılıyor. Bu ayarlara g&amp;ouml;re JPA&lt;em&gt;(Java Persistence API)&lt;/em&gt; nesneleri ORM tarafında hibernate kullanacak ve veritabanı şeması flyway ile y&amp;ouml;netilecek. Ayrıca loglama seviyesini debug olarak ayarladık ki SQL sorgularını loglarda g&amp;ouml;rebilelim&lt;em&gt;(Bunu sadece development aşamasında a&amp;ccedil;mak ama &amp;uuml;retim ortamında kapatmak iyi bir pratiktir)&lt;/em&gt;. Son olarak `ddl-auto` değerini validate olarak ayarladık. Bu sayede JPA, uygulama başlatıldığında veritabanı şemasını kontrol edecek ve eğer tablolar eksikse veya hatalıysa uygulama başlatılmayacaktır. Bu sayede veritabanı şemasının her zaman g&amp;uuml;ncel ve doğru olmasını garanti edebiliriz.&lt;/p&gt;
&lt;h2&gt;Migration Hazırlıkları&lt;/h2&gt;
&lt;p&gt;Migration işlemleri i&amp;ccedil;in `src/main/resources/db/migration` klas&amp;ouml;r&amp;uuml;nde `V1__init.sql` isimli bir dosya oluşturalım ve i&amp;ccedil;eriğini aşağıdaki gibi hazırlayalım. Bu basit &amp;ccedil;alışma i&amp;ccedil;in &amp;ccedil;oğul eki&lt;em&gt;(plural)&lt;/em&gt; formatında isimlendirilmiş games ve rentals tabloları yeterli olacaktır.&lt;/p&gt;
&lt;pre class="brush:sql;auto-links:false;toolbar:false" contenteditable="false"&gt;CREATE TABLE games (
    id               UUID         PRIMARY KEY,
    title            VARCHAR(100) NOT NULL,
    platform         VARCHAR(50)  NOT NULL,
    total_copies     INT          NOT NULL,
    available_copies INT          NOT NULL
);

CREATE TABLE rentals (
    id                 UUID          PRIMARY KEY,
    game_id            UUID          NOT NULL,
    member_id          UUID          NOT NULL,
    rented_on          DATE          NOT NULL,
    due_on             DATE          NOT NULL,
    returned_on        DATE,
    status             VARCHAR(20)   NOT NULL,
    late_fee_amount    NUMERIC(10,2) NOT NULL,
    late_fee_currency  VARCHAR(3)    NOT NULL
);

CREATE INDEX idx_rentals_game_id ON rentals (game_id);&lt;/pre&gt;
&lt;p&gt;DDD tasarımında her aggregate kendi tutarlı b&amp;uuml;t&amp;uuml;nl&amp;uuml;ğ&amp;uuml;n&amp;uuml; korumaktan sorumludur. Genellikle bir aggregate diğer aggregate'lere ID ile referans verir. Bu nedenle herhangi bir foreign key constraint kullanmamıza gerek yok. B&amp;ouml;ylece aggregate'ler birbirlerinden bağımsız olur ve aggregate root'lar kendi aggregate'lerini y&amp;ouml;netebilirler. Zaten foreign key constraint kullandığımızda aggregate'ler DB seviyesinde birbirlerinin yaşam d&amp;ouml;ng&amp;uuml;s&amp;uuml;ne bağımlı hale gelir ve bu DDD tasarımına aykırıdır.&lt;/p&gt;
&lt;h2&gt;Docker-Compose Olmadan Asla&lt;/h2&gt;
&lt;p&gt;Pek &amp;ccedil;ok kişisel ve uzun soluklu &amp;ccedil;alışmamdan alışılageldiği &amp;uuml;zere burada da bir docker-compose dosyası ile PostgreSQL veritabanını ve varsa başka gereklilikleri ayağa kaldırmayı tercih ediyorum. İşte `docker-compose.yml` i&amp;ccedil;eriğimiz;&lt;/p&gt;
&lt;pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"&gt;services:

  postgres:
    image: postgres:latest
    container_name: spring-postgres
    environment:
      POSTGRES_USER: johndoe
      POSTGRES_PASSWORD: somew0rds
      POSTGRES_DB: postgres
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql
    networks:
      - spring-network

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: spring-pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: scoth@tiger.com
      PGADMIN_DEFAULT_PASSWORD: 123456
    ports:
      - "5050:80"
    depends_on:
      - postgres
    networks:
      - spring-network

  sonarqube:
    image: sonarqube:community
    container_name: spring-sonarqube
    restart: unless-stopped
    stop_grace_period: 1h
    ports:
      - "9000:9000"
    environment:
      SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: "true"
    volumes:
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_logs:/opt/sonarqube/logs
      - sonarqube_extensions:/opt/sonarqube/extensions

volumes:
  postgres_data:
  sonarqube_data:
  sonarqube_logs:
  sonarqube_extensions:

networks:
  spring-network:
    driver: bridge&lt;/pre&gt;
&lt;p&gt;Haftasonu yapabileceğiniz t&amp;uuml;rden olan bu tip &amp;ccedil;alışmalarda ideal bir pratik olduğunu d&amp;uuml;ş&amp;uuml;n&amp;uuml;yorum. İşiniz bitince konteynerleri kapatır veya silersiniz. Tavsiye edeceğim bir diğer alışkanlıksa temiz kod prensiplerine uymak ve teknik borcu en başından itibaren kontrol altına almak. Bu nedenle Sonarqube servisini de docker-compose i&amp;ccedil;erisine ekledim. Sizden sonraki yazılımcıyı d&amp;uuml;ş&amp;uuml;nerek kod yazın l&amp;uuml;tfen :D Konteynerleri ayağa kaldırmak i&amp;ccedil;in terminalden aşağıdaki komutu kullanabiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;sudo docker-compose up -d&lt;/pre&gt;
&lt;h2&gt;Uygulama Kodlarımız&lt;/h2&gt;
&lt;p&gt;Artık sırasıyla uygulama i&amp;ccedil;erisindeki gerekli enstr&amp;uuml;manları yazmaya başlayabiliriz. Tabii artık Java d&amp;uuml;nyasındayız. Efsane paket sistemi URL benzeri bir yapıya sahip. Bu proje a&amp;ccedil;ısından baktığımızda root paketimiz `com.example.gamerental` şeklinde. Diğer t&amp;uuml;m alt mod&amp;uuml;lleri bu paket altına a&amp;ccedil;acağız. Kabaca aşağıdaki gibi bir dizin yapısı oluşturmamız gerektiğini ifade edebiliriz.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDSkeleton_00.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Yazıyı &amp;ccedil;ok fazla uzatmaması i&amp;ccedil;in kod dosyalarının i&amp;ccedil;eriklerini buraya eklemedim. Dolayısıyla sizi g&amp;uuml;zel bir m&amp;uuml;cadele bekliyor. &lt;a href="https://github.com/buraksenyurt/enterprise-patterns-with-java/tree/main/src/DDD" target="_blank"&gt;Kodları ve projeyi buradan &amp;ccedil;ekip&lt;/a&gt; inceleyebilir veya sıfırdan kendi senaryonuzla yazabilirsiniz. Kod dosyalarına olabildiğince yorum satırı ekledim umarım anlaşılır olur.&lt;/p&gt;
&lt;h2&gt;C# Geliştiricileri A&amp;ccedil;ısından&lt;/h2&gt;
&lt;p&gt;Bu &amp;ccedil;alışma &amp;ouml;zelinde bir karşılaştırma tablosu da eklemek isterim. Kodları yazarken veya yazdıktan sonra işinize yarayabilir. En azından Java tarafında kullandığımız enstr&amp;uuml;man veya kavramlar C# tarafında neye karşlık geliyor &amp;ouml;zetleyebiliriz.&lt;/p&gt;
&lt;table border="1"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mesele&lt;/th&gt;
&lt;th&gt;.NET/C#&lt;/th&gt;
&lt;th&gt;Java/Spring Boot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Proje oluşturma&lt;/td&gt;
&lt;td&gt;dotnet new CLI&lt;/td&gt;
&lt;td&gt;Spring Initializr veya curl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bağımlılık y&amp;ouml;netimi&lt;/td&gt;
&lt;td&gt;.csproj i&amp;ccedil;inde NuGet&lt;/td&gt;
&lt;td&gt;pom.xml i&amp;ccedil;inde Maven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ORM&lt;/td&gt;
&lt;td&gt;Entity Framework Core&lt;/td&gt;
&lt;td&gt;Spring Data JPA + Hibernate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration&lt;/td&gt;
&lt;td&gt;EF Core Migrations&lt;/td&gt;
&lt;td&gt;Flyway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value Object&lt;/td&gt;
&lt;td&gt;record&lt;/td&gt;
&lt;td&gt;record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity kimliği&lt;/td&gt;
&lt;td&gt;Dahili Guid desteği&lt;/td&gt;
&lt;td&gt;UUID sınıfı&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;Ccedil;alıştırma&lt;/td&gt;
&lt;td&gt;dotnet run&lt;/td&gt;
&lt;td&gt;mvnw spring-boot:run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Framework&lt;/td&gt;
&lt;td&gt;xUnit, NUnit, MSTest&lt;/td&gt;
&lt;td&gt;JUnit 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inheritance&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:&lt;/code&gt; operat&amp;ouml;r&amp;uuml;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extends&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface uygulama&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:&lt;/code&gt; operat&amp;ouml;r&amp;uuml;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;implements&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paket kavramı&lt;/td&gt;
&lt;td&gt;GameRental şeklinde namespace&lt;/td&gt;
&lt;td&gt;com.example.gamerental şeklinde package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class Yerleşimi&lt;/td&gt;
&lt;td&gt;Bir dosya i&amp;ccedil;inde birden fazla class olabilir&lt;/td&gt;
&lt;td&gt;Her class kendi java dosyasında olmalı&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opsiyonel değerler&lt;/td&gt;
&lt;td&gt;Nullable reference types, &lt;code&gt;?&lt;/code&gt; operat&amp;ouml;r&amp;uuml;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt; sınıfı&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;br /&gt;Bununla birlikte kodlarda Spring Boot d&amp;uuml;nyasından gelen bir&amp;ccedil;ok anotasyon&lt;em&gt;(annotation)&lt;/em&gt; kullanımı var. Bunlarla ilgili bir eşleştirmeyi de aşağıdaki tabloda bulabilirsiniz.&lt;/p&gt;
&lt;table border="1"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Konu&lt;/th&gt;
&lt;th&gt;Spring/Java&lt;/th&gt;
&lt;th&gt;.NET/C#&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bir şeyin Entity olduğunu belirtmek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Entity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Genelde bir Entity sınıfından t&amp;uuml;retmek&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB tarafındaki tabloyu belirtmek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[Table]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bir entity alanının identity olduğunu belirtmek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[Key]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bir entity alanının enum olduğunu belirtmek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Enumerated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[EnumDataType]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repository olarak kullanılacağını belirtmek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Repository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Repository deseni ve DbContext&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service olduğunu belirtmek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tam karşılığı yok sanırım&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction y&amp;ouml;netimine dahil olduğunu s&amp;ouml;ylemek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Transactional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TransactionScope veya DbContext &amp;uuml;zerinden Transaction y&amp;ouml;netimi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doğrulama (Validation)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Valid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[Required]&lt;/code&gt;, &lt;code&gt;[Range]&lt;/code&gt; gibi Data Annotation'lar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;Uuml;st t&amp;uuml;r metodunu ezmek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Override&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;override&lt;/code&gt; anahtar kelimesi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST t&amp;uuml;r&amp;uuml;nden bir controller&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@RestController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ApiController]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request mapping&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@RequestMapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[Route]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request body&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@RequestBody&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[FromBody]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path variable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@PathVariable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[FromRoute]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP Post, Get, Put, Delete gibi aksiyonlar&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@PostMapping&lt;/code&gt;, &lt;code&gt;@GetMapping&lt;/code&gt;, &lt;code&gt;@PutMapping&lt;/code&gt;, &lt;code&gt;@DeleteMapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[HttpPost]&lt;/code&gt;, &lt;code&gt;[HttpGet]&lt;/code&gt;, &lt;code&gt;[HttpPut]&lt;/code&gt;, &lt;code&gt;[HttpDelete]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;br /&gt;Arada ka&amp;ccedil;ırdıklarım olabilir ancak Spring Boot &amp;ccedil;atısının bir&amp;ccedil;ok anotasyon ile işimizi kolaylaştırdığını s&amp;ouml;yleyebilirim. Sanıyorum ki boilerplate kod yazmak zorunda kalmıyoruz.&lt;/p&gt;
&lt;h2&gt;&amp;Ccedil;alışma Zamanı&lt;/h2&gt;
&lt;p&gt;Projeyi tamamladıktan sonraki olası sonu&amp;ccedil;larımı paylaşarak devam edelim. Beni en &amp;ccedil;ok d&amp;uuml;ş&amp;uuml;nd&amp;uuml;ren şeylerden birisi nasıl build alacağım veya &amp;ccedil;alıştıracağım idi. .NET tarafında dotnet, rust d&amp;uuml;nyasında cargo gibi tekil ara&amp;ccedil;lara alıştığım i&amp;ccedil;in Java tarafındaki maven, gradle gibi ara&amp;ccedil; &amp;ccedil;okluğu biraz kafa karıştırıcı olabiliyor. Ancak ne nihyatetinde hedefim projeyi &amp;ccedil;alıştırmak olduğundan detaylarda &amp;ccedil;ok fazla boğulmadan ilerledim ve aşağıdaki maven komutu ile projeyi başlattım. Tabii postgresql konteynerinin &amp;ccedil;alışır durumda olduğundan emin olmalıyız ve s&amp;ouml;z konusu komutu projenin k&amp;ouml;k dizininde işletmeliyiz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;# Derleme i&amp;ccedil;in
./mvnw compile

# Doğrudan &amp;ccedil;alıştırmak i&amp;ccedil;in
./mvnw spring-boot:run&lt;/pre&gt;
&lt;p&gt;Başlangı&amp;ccedil;ta `V1__init.sql` dosyasındaki SQL scriptleri &amp;ccedil;alışacak ve veritabanında gerekli tablolar oluşturulacaktır. Daha sonra API endpoint'lerine&lt;em&gt;(interfaces katmanındaki Controller sınıflarımız)&lt;/em&gt; aşina olduğumuz HTTP isteklerini g&amp;ouml;ndererek uygulama &amp;ccedil;ıktılarını sorgulayabiliriz. İşte birka&amp;ccedil; deneme;&lt;/p&gt;
&lt;p&gt;Tabii işin gereği kiralanacak oyunlar lazım. Efsanelerden Super Mario'yu kataloğumuza ekleyerek testlere başlayabiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;curl -X POST http://localhost:8080/api/games \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Super Mario",
        "platform": "NINTENDO_SWITCH",
        "totalCopies": 5
      }'&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDRuntime_00.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;ve şimdi de bir oyun kiralayalım. Tabii tam bir abonelik sistemimiz olmadığı i&amp;ccedil;in abone ID bilgisini kendimiz veriyoruz. Game ID değeri i&amp;ccedil;inse bir &amp;ouml;nceki denemede eklediğimiz ID değerini kullanabiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;curl -s -X POST http://localhost:8080/api/rentals \
  -H 'Content-Type: application/json' \
  -d "{\"gameId\":\"3c23c76e-3902-40c6-8424-366196f669ca\",\"memberId\":\"de4b0278-b1fd-4dc1-a05b-2509107fd49b\",\"rentalDays\":10}"&lt;/pre&gt;
&lt;p&gt;Şimdide kullanılabilecek oyun kopya sayısını kontrol edebiliriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;curl -s http://localhost:8080/api/games/3c23c76e-3902-40c6-8424-366196f669ca&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDRuntime_01.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Abonemizin oyunu &amp;ccedil;ok sevdiğini ve birka&amp;ccedil; g&amp;uuml;n ge&amp;ccedil; iade ettiğini d&amp;uuml;ş&amp;uuml;nelim. Bunu sim&amp;uuml;le etmek i&amp;ccedil;in aşağıdaki gibi ilerleyebiriz.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;# &amp;Ouml;nce iade tarihini bug&amp;uuml;nden 15 sonraya ayarlayalım ki gecikme &amp;uuml;creti oluşsun.
LATE_DATE=$(date -d "+ 15 days" +%F)

# Bu &amp;ccedil;ağrı sonrasında bir gecikme &amp;uuml;creti oluşmasını bekliyoruz.
curl -s -X POST http://localhost:8080/api/rentals/040cf074-9fc2-4480-9cc5-10a258efe7df/return \
  -H 'Content-Type: application/json' \
  -d "{\"returnDate\":\"$LATE_DATE\"}"

# Son olarak oyun bilgilerini tekrar kontrol edelim. Kiralama sona erdiği i&amp;ccedil;in kullanılabilir oyun sayısının tekrar 5 olduğunu g&amp;ouml;rmemiz lazım.
curl -s http://localhost:8080/api/games/3c23c76e-3902-40c6-8424-366196f669ca&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDRuntime_02.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Son olarak birde d&amp;uuml;kkana geri d&amp;ouml;nen bir oyunu tekrar d&amp;ouml;nd&amp;uuml;rmek istediğimiz almamız gereken Conflict hatasına bakalım.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;curl -i -s -X POST http://localhost:8080/api/rentals/040cf074-9fc2-4480-9cc5-10a258efe7df/return \
  -H 'Content-Type: application/json' -d '{}'&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDRuntime_03.png" alt="" /&gt;&lt;/p&gt;
&lt;h2&gt;Testler&lt;/h2&gt;
&lt;p&gt;API noktaları sorunsuz &amp;ccedil;alışıyor gibi ama kurumsal &amp;ccedil;apta bir projenin olmazsa olmazlarından birisi de elbetteki birim testler&lt;em&gt;(Unit Tests)&lt;/em&gt;. Birim testler Code Coverage a&amp;ccedil;ısından &amp;ouml;nemlidir. Bu sayede kodun hangi par&amp;ccedil;alarının test edildiği yani &amp;uuml;zerinden ge&amp;ccedil;ilerek sağlamasının yapıldığı y&amp;uuml;zdesel olarak &amp;ouml;l&amp;ccedil;&amp;uuml;lebilir ki bu kodun g&amp;uuml;venilirliğinin bir garantisidir. Projemizdeki testler `test/java/com/example/gamerental` dizininde yer almaktadır. &amp;Ouml;rnek olması a&amp;ccedil;ısından Game ve Rental nesnelerinin domain bazlı iş kurallarını test ediyoruz. Projedeki t&amp;uuml;m testleri &amp;ccedil;alıştırmak i&amp;ccedil;in k&amp;ouml;k dizindeyken aşağıdaki komutu kullanmamız yeterli.&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;./mvnw test&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDRuntime_04.png" alt="" /&gt;&lt;/p&gt;
&lt;h2&gt;Sonarqube ile Kod Taraması&lt;/h2&gt;
&lt;p&gt;Dilerseniz birde yazdığımız kod tabanının r&amp;ouml;ntgenini &amp;ccedil;ekip projenin sağlık durumunu kontrol edelim. &amp;Ouml;ncelikle `docker-compose.yml` i&amp;ccedil;erisinde tanımladığımız Sonarqube servisinin &amp;ccedil;alıştığından emin olalım. Ayarlarımıza g&amp;ouml;re `localhost:9000` adresine gidip Sonarqube aray&amp;uuml;z&amp;uuml;ne ulaşabiliyorsak her şey yolunda demektir. Sonrasında aşağıdaki adımları takip edebilirz.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`Create a local project` ile ilerleyelim: Project display name ve Project key değerlerini game-rental olarak belirleyebiliriz. Bir değişiklik yoksa Main branch name değerini main olarak bırakabiliriz.&lt;/li&gt;
&lt;li&gt;`Set up new code for project` adımında Follows the instance's default se&amp;ccedil;eneğini işaretleyip Create Project butonuna tıklayalım.&lt;/li&gt;
&lt;li&gt;`Analysis Method` adımında birka&amp;ccedil; se&amp;ccedil;enek g&amp;ouml;rebiliriz. Bunlar genellikle uzak kod repolarına bağlanabileceğimiz alternatiflerdir. Biz local ortamda ilerlediğimiz i&amp;ccedil;in Locally se&amp;ccedil;imi ile devam edelim.&lt;/li&gt;
&lt;li&gt;`Analyze your project` kısmında &amp;ouml;ncelikle bir token &amp;uuml;retmemiz gerekiyor. Ger&amp;ccedil;ek hayat senarylarında belirli s&amp;uuml;relerde token değerinin değiştirilmesi istenir ancak bu eğitim projesinde Expires in se&amp;ccedil;eneğini No expiration olarak bırakabiliriz. Token değerini oluşturduktan sonra Continue tuşuna basarak devam edelim.&lt;/li&gt;
&lt;li&gt;`Run analysis on your project` adımında projemizin Java tabanlı olduğunu belirtmemiz gerekiyor. &amp;Ouml;rneğimizde build tool olarak Maven kullanıldığı i&amp;ccedil;in onu işaretleyebiliriz.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;T&amp;uuml;m bu adımları tamamladığımızda terminalden &amp;ccedil;alıştıracağımız bir komutumuz olacaktır. Bunu yine projenin root klas&amp;ouml;r&amp;uuml;nde işletmemiz gerekiyor&lt;em&gt;(Token bilgisi az &amp;ouml;nce &amp;uuml;retilen bilgidir ve siz denerken farklı bir token olacaktır)&lt;/em&gt;&lt;/p&gt;
&lt;pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"&gt;mvn clean verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \
  -Dsonar.projectKey=game-rental \
  -Dsonar.projectName='game-rental' \
  -Dsonar.host.url=http://localhost:9000 \
  -Dsonar.token=sqp_859e5004eaccc5c0bb1b2a7f3ada6b5b0c26ce54&lt;/pre&gt;
&lt;p&gt;İşte projenin teknik değerlendirmesine dair ilk MR raporu. Her ne kadar Passed olarak işaretlense de bir&amp;ccedil;ok uyarı var.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.buraksenyurt.com/image.axd?picture=/2026/Haziran/JDDDSonarqube_00.png" alt="" /&gt;&lt;/p&gt;
&lt;h2&gt;Ya Sonrası?&lt;/h2&gt;
&lt;p&gt;Buraya kadar sabırla geldiyseniz ve benzer &amp;ccedil;ıktılara ulaştıysanız sizi tebrik ederim. En azından bu haftasonunu iyi bir şekilde değerlendirmiş olduğunuzu d&amp;uuml;ş&amp;uuml;n&amp;uuml;yorum. Tabii ki bu &amp;ccedil;alışma benim gibi C# programcıları i&amp;ccedil;in başlangı&amp;ccedil; seviyesinde bir pratik. Dil bağımsız konuşmak gerekirse, DDD yaklaşımının ger&amp;ccedil;ek hayat senaryolarında uygulanması sanıldığı kadar kolay da değil. &amp;Ouml;zellikle domain b&amp;uuml;y&amp;uuml;d&amp;uuml;k&amp;ccedil;e, farklı domain'ler işin i&amp;ccedil;erisine dahil olduk&amp;ccedil;a, iş kuralları karmaşıklaştık&amp;ccedil;a kurgu giderek zorlaşabiliyor. &amp;Ouml;rneğin kim aggregate olmalı veya aggregate root olarak d&amp;uuml;ş&amp;uuml;n&amp;uuml;lmeli, kim Value Object olmalı, kim Entity olmalı, hangi iş kuralları aggregate i&amp;ccedil;erisinde yaşamalı, hangileri Domain Service i&amp;ccedil;erisinde konuşlanmalı gibi sorulara cevap bulmak kolay değil. Bu nedenle DDD yaklaşımını &amp;ouml;ğrenmek i&amp;ccedil;in bolca pratik yapmak veya uygulandığı a&amp;ccedil;ık kaynak projeleri incelemek gerekiyor. Peki bu &amp;ccedil;alışma &amp;uuml;zerine başka neler neler yapabiliriz?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;Uuml;ye nesnesini ger&amp;ccedil;ek bir aggregate' e d&amp;ouml;n&amp;uuml;şt&amp;uuml;rmek. &amp;Ouml;rneğin bir &amp;uuml;ye en fazla 3 kiralama yapabilir gibi bir senaryoyu ele almak.&lt;/li&gt;
&lt;li&gt;Domain bazlı event'ler eklemek. GameRented, GameReturned gibi event'ler kurgulayıp &amp;ouml;rneğin bildirim g&amp;ouml;ndermek veya iki aggregate &amp;uuml;zerinden transactional bir senaryo kurgulamak.&lt;/li&gt;
&lt;li&gt;İkinci bir bounded context ile &amp;ccedil;alışmak. &amp;Ouml;deme&lt;em&gt;(Billing)&lt;/em&gt; olabilir mesela. Renting diye farklı bir bounded context ile de event'ler yardımıyla haberleşebiliriz. DDD'nin stratejik tasarımını sahada deneyimleriz.&lt;/li&gt;
&lt;li&gt;Mapping operasyonları i&amp;ccedil;in farklı alternatifler aramak. &amp;Ouml;rneğin MapStruct gibi bir enstr&amp;uuml;manı işin i&amp;ccedil;erisine katabiliriz.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;B&amp;ouml;ylece geldik bir makalemizin daha sonuna. Tekrardan g&amp;ouml;r&amp;uuml;ş&amp;uuml;nceye dek hepinize mutlu g&amp;uuml;nler dilerim.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/buraksenyurt/farm-java-ddd" target="_blank"&gt;Orjinal repoya ve kodlara github &amp;uuml;zerinden erişebilirsiniz&lt;/a&gt;&lt;/p&gt;</summary>
    <published>2026-06-20T09:47:00+00:00</published>
    <link rel="related" href="https://www.buraksenyurt.com/post/spring-boot-ile-basit-bir-ddd-projesi#comment" />
    <category term="Java" />
    <betag:tag>java</betag:tag>
    <betag:tag>maven</betag:tag>
    <betag:tag>domain driven design</betag:tag>
    <betag:tag>ddd</betag:tag>
    <betag:tag>hibernate</betag:tag>
    <betag:tag>docker compose</betag:tag>
    <betag:tag>postgresql</betag:tag>
    <betag:tag>layered architecture</betag:tag>
    <betag:tag>clean code</betag:tag>
    <betag:tag>spring</betag:tag>
    <betag:tag>spring boot</betag:tag>
    <betag:tag>spring framework</betag:tag>
    <betag:tag>spring initializr</betag:tag>
    <dc:publisher>bsenyurt</dc:publisher>
    <dc:description>Amacım Domain Driven Design (DDD) yaklaşımını yine hafifsiklet bir senaryo ile olabildiğince basit bir şekilde ele almak (CQRS, Event Sourcing, Message Bus gibi konuları sonraya bırakıyorum) ancak bu sefer Java programlama dilini ve Spring Boot Framework kullanacağım. Başlarken Java dilinde duayen olan arkadaşlarımın affına sığınıyorum zira bir C# geliştiricisi personasıyla olaylara yaklaşacağım. Dolayısıyla Java'nın ve Spring Boot'un bazı niş yeteneklerini atlayabilirim. Bu konuda yorumlarla bana ve okurlarımıza destek olabilirsiniz. Öyleyse başlayalım.</dc:description>
    <pingback:server>https://www.buraksenyurt.com/pingback.axd</pingback:server>
    <pingback:target>https://www.buraksenyurt.com/post.aspx?id=6aeb67b9-88f8-4b2e-a104-b2c76526aa95</pingback:target>
    <slash:comments>0</slash:comments>
    <trackback:ping>https://www.buraksenyurt.com/trackback.axd?id=6aeb67b9-88f8-4b2e-a104-b2c76526aa95</trackback:ping>
    <wfw:comment>https://www.buraksenyurt.com/post/spring-boot-ile-basit-bir-ddd-projesi#comment</wfw:comment>
    <wfw:commentRss>https://www.buraksenyurt.com/syndication.axd?post=6aeb67b9-88f8-4b2e-a104-b2c76526aa95</wfw:commentRss>
  </entry>
</feed>