Mutable Kayıtların Denetim Altında Nasıl Bozulduğu
Çoğu web uygulaması, bir kaydın güncel durumunu saklar. Örneğin, bir fatura durumu “beklemede” iken “ödenmiş” durumuna geçtiğinde, veritabanı satırı yerinde güncellenir. Önceki durum kaybolur.
Bu, çoğu uygulama için sorun yaratmaz. Ancak finansal sistemler için ciddi bir risk teşkil eder:
- Tarihçe yok: Faturanın durumunun dün saat 15:00’te ne olduğunu kanıtlayamazsınız.
- Atıf yok: Durumu kimin ve neden değiştirdiğini belirleyemezsiniz.
- Uzlaştırma yolu yok: Sayılar eşleşmediğinde, takip edilecek bir iz yoktur.
- Manipülasyon kanıtı yok: Değiştirilmiş bir kayıt, yasal görünür.
Finansal düzenlemeler — PCI DSS, SOX, FCA yönergeleri — sistemlerin tüm finansal işlemler ve durum değişiklikleri için tam, değiştirilemez bir kayıt tutmasını gerektirir. Bir UPDATE ifadesi, saklamanız gereken kanıtları tam olarak yok eder.
Değiştirilemez Kayıt Deseni
Çözüm, finansal kayıtları asla güncellememek veya silmemektir. Bunun yerine, her değişiklik yeni, eklenebilir bir olay olarak kaydedilir:
[Olay 1] Fatura #1042 OLUŞTURULDU — tutar: £5,000 — kimden: user_42 — saat: 2025-03-01T09:15:00Z
[Olay 2] Fatura #1042 ONAYLANDI — onaylayan: user_15 — saat: 2025-03-01T14:30:00Z
[Olay 3] Fatura #1042 ÖDEME_ALINDI — tutar: £5,000 — referans: PAY_8821 — saat: 2025-03-05T11:00:00Z
[Olay 4] Fatura #1042 UYUMLANDI — bankayla eşleşen: BANK_TXN_4401 — saat: 2025-03-06T08:45:00Z
Faturanın güncel durumu, olayların sırasını yeniden oynatarak elde edilir. Denetim yolu, olayların kendisidir. Hiçbir şey kaybolmaz, hiçbir şey üzerine yazılmaz.
Mimari Genel Görünüm
[Müşteri Talebi]
|
v
[Uygulama Katmanı]
- İş kurallarını doğrula
- Olay kaydını oluştur
|
v
[DB İşlemi]
- Olayı audit_events tablosuna yaz (sadece ekleme)
- Materyalize edilmiş görünümü / güncel durum tablosunu güncelle
|
v
[Denetim Olayları Tablosu] [Güncel Durum Tablosu]
(değiştirilemez, yalnızca ekleme) (türetilebilir, yeniden oluşturulabilir)
|
v
[Asenkron Tüketiciler]
- Uyum raporlama
- Uzlaştırma motoru
- Bildirim servisi
Kritik tasarım kararı: denetim olayları tablosu gerçeğin kaynağıdır. Güncel durum tablosu, olaylardan herhangi bir zamanda yeniden inşa edilebilecek bir projeksiyondur.
Laravel’de Uygulama
Denetim Olayları Göçü
Schema::create('audit_events', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('aggregate_type');
$table->string('aggregate_id');
$table->unsignedBigInteger('sequence_number');
$table->string('event_type');
$table->json('payload');
$table->json('metadata');
$table->string('actor_id');
$table->string('actor_type');
$table->string('ip_address')->nullable();
$table->string('checksum');
$table->timestamp('occurred_at');
$table->timestamp('created_at');
$table->unique(['aggregate_type', 'aggregate_id', 'sequence_number']);
$table->index(['aggregate_type', 'aggregate_id']);
$table->index(['event_type']);
$table->index(['occurred_at']);
$table->index(['actor_id']);
});checksum sütunu, olay verisinin ve önceki olayın checksum’ının bir hash’ini saklar. Geçmiş bir kayıtta yapılan herhangi bir değişiklik zinciri bozar.
Denetim Olayı Servisi
namespace App\Services;
use App\Models\AuditEvent;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class AuditEventService
{
public function record(
string $aggregateType,
string $aggregateId,
string $eventType,
array $payload,
string $actorId,
string $actorType = 'user',
?string $ipAddress = null,
): AuditEvent {
return DB::transaction(function () use (
$aggregateType,
$aggregateId,
$eventType,
$payload,
$actorId,
$actorType,
$ipAddress
) {
$lastEvent = AuditEvent::where('aggregate_type', $aggregateType)
->where('aggregate_id', $aggregateId)
->orderByDesc('sequence_number')
->lockForUpdate()
->first();
$sequenceNumber = $lastEvent ? $lastEvent->sequence_number + 1 : 1;
$previousChecksum = $lastEvent ? $lastEvent->checksum : 'GENESIS';
$eventData = [
'aggregate_type' => $aggregateType,
'aggregate_id' => $aggregateId,
'sequence_number' => $sequenceNumber,
'event_type' => $eventType,
'payload' => $payload,
'actor_id' => $actorId,
'occurred_at' => now(),
];
$checksum = hash('sha256', $previousChecksum . json_encode($eventData));
return AuditEvent::create([
'id' => Str::uuid(),
...$eventData,
'metadata' => [
'previous_checksum' => $previousChecksum,
'schema_version' => '1.0',
],
'actor_type' => $actorType,
'ip_address' => $ipAddress,
'checksum' => $checksum,
'created_at' => now(),
]);
});
}
public function verifyChain(string $aggregateType, string $aggregateId): bool
{
$events = AuditEvent::where('aggregate_type', $aggregateType)
->where('aggregate_id', $aggregateId)
->orderBy('sequence_number')
->get();
$previousChecksum = 'GENESIS';
foreach ($events as $event) {
$eventData = [
'aggregate_type' => $event->aggregate_type,
'aggregate_id' => $event->aggregate_id,
'sequence_number' => $event->sequence_number,
'event_type' => $event->event_type,
'payload' => $event->payload,
'actor_id' => $event->actor_id,
'occurred_at' => $event->occurred_at,
];
$expectedChecksum = hash('sha256', $previousChecksum . json_encode($eventData));
if ($expectedChecksum !== $event->checksum) {
return false;
}
$previousChecksum = $event->checksum;
}
return true;
}
public function getHistory(string $aggregateType, string $aggregateId): \Illuminate\Support\Collection
{
return AuditEvent::where('aggregate_type', $aggregateType)
->where('aggregate_id', $aggregateId)
->orderBy('sequence_number')
->get();
}
}Finansal Bir İş Akışında Kullanımı
class InvoiceService
{
public function __construct(
private AuditEventService $auditService,
) {}
public function createInvoice(array $data, string $userId, string $ip): Invoice
{
return DB::transaction(function () use ($data, $userId, $ip) {
$invoice = Invoice::create([
'number' => $this->generateInvoiceNumber(),
'client_id' => $data['client_id'],
'amount' => $data['amount'],
'currency' => $data['currency'],
'status' => 'draft',
'due_date' => $data['due_date'],
]);
$this->auditService->record(
aggregateType: 'invoice',
aggregateId: (string)$invoice->id,
eventType: 'invoice.created',
payload: [
'number' => $invoice->number,
'amount' => $invoice->amount,
'currency' => $invoice->currency,
'client_id' => $invoice->client_id,
],
actorId: $userId,
ipAddress: $ip,
);
return $invoice;
});
}
public function approveInvoice(Invoice $invoice, string $approverId, string $ip): Invoice
{
return DB::transaction(function () use ($invoice, $approverId, $ip) {
$invoice->update(['status' => 'approved']);
$this->auditService->record(
aggregateType: 'invoice',
aggregateId: (string)$invoice->id,
eventType: 'invoice.approved',
payload: [
'previous_status' => 'draft',
'new_status' => 'approved',
],
actorId: $approverId,
ipAddress: $ip,
);
return $invoice->fresh();
});
}
}Uygulama Sonuçları
Finansal platformlarda değiştirilemez denetim kaydı uygulandığında:
| Metrik | Önce | Sonra |
|---|---|---|
| Uzlaştırma süresi (haftalık) | 4+ saat manuel | 15 dakika otomatik |
| Denetim hazırlık süresi | 2-3 hafta | 2 gün |
| Çözümsüz farklılıklar | Aylık 8-12 | Aylık 0-1 |
| Uyum denetimi bulguları | Her incelemede birden fazla | Hiçbir maddi bulgu yok |
| Veri uyuşmazlığı çözülme süresi | 3-5 gün |
Anahtar Noktalar
Finansal kayıtları asla yerinde güncellemeyin. Bunun yerine yeni olaylar ekleyin. Güncel durum, olay geçmişinin bir projeksiyonudur.
Manipülasyon kanıtı için hash zincirleri kullanın. Her olayın checksum’ı, önceki olayın checksum’ını içerir. Herhangi bir değişiklik zinciri bozar.
Kim, ne, ne zaman ve neden kaydedin. Her olay bir aktör, bir zaman damgası ve iş bağlamı içermelidir. IP adresleri ve oturum tanımlayıcıları, adli değer katar.
Gün başından itibaren süreyi planlayın. Denetim kayıtları hızla büyür. Katmanlı depolama (hot/warm/cold) maliyetleri yönetilebilir tutarken uyum gereksinimlerini karşılar.
Otomatik uzlaştırma yapın. Değiştirilemez kayıtlar, saatler süren manuel tablo karşılaştırmasını ortadan kaldıran programatik uzlaştırmayı mümkün kılar.
Olayları ve iş verilerini aynı işlem içinde yazın. Eğer atomik değillerse, gerçeğiniz ile denetim iziniz arasındaki tutarsızlık riski taşır.
Sonuç
Denetim kanıtı niteliğindeki finansal iş akışları, daha fazla günlük üretmekle ilgili değildir — her durum değişikliğinin kaydedildiği, zincirlendiği ve doğrulanabilir olduğu sistemler tasarlamakla ilgilidir. Sadece ekleme yazımları, hash-zinciri bütünlüğü ve katmanlı süreler, herhangi bir denetçinin sorusuna kanıt ile, açıklama değil, cevap verir.
Laravel veya Node.js ile inşa etseniz de, yapılar aynıdır. Bu altyapı, her defasında bir düzenleyici soru sorduğunda veya bir farklılık çözülmesi gerektiğinde değerini öder.
Kaynak: Orijinal Makale


