Stok Takip Sistemi
Ürün, stok, satış, müşteri ve tedarikçi süreçlerini tek çatı altında toplayan katmanlı bir Web API.
- Rol
- Süre
- Teknoloji

Problem
Küçük işletmelerin çoğu stoğunu hâlâ elektronik tabloda tutuyor. Satış kaydı bir yerde, stok sayısı başka bir yerde duruyor; ikisi arasındaki bağ insan eliyle kuruluyor. Sonuç her zaman aynı: kâğıt üstünde var olan ama rafta olmayan ürünler.
Hedef
Satış ile stoğu tek bir işlemin içine almak, iş kurallarını controller'lardan çıkarıp kendi katmanına taşımak ve veritabanı şemasını API sözleşmesinden ayırmak.
Mimari
- 01
API
İnce controller'lar. Yalnızca HTTP, doğrulama ve DTO eşlemesi.
- Controllers
- DTO
- Validation
- Swagger
- 02
Business
Tüm iş kuralları burada. Bu katman HTTP'yi tanımıyor.
- Services
- Rules
- Mapping
- 03
Data Access
EF Core üzerinden veriye tek giriş noktası.
- Repository
- DbContext
- Migrations
- 04
Domain
Varlıklar ve enum'lar. Hiçbir katmana bağımlı değil.
- Product
- Stock
- Sale
- Customer
- Supplier
- 05
PostgreSQL
İlişkisel şema, yabancı anahtarlar, indeksler.
- Tables
- FK
- Index
Teknik kararlar
- SeçilenPostgreSQLYerineMS SQL Server
Daha önce MS SQL kullanıyordum, ama bu proje bir işletmede çalışacaktı. Lisans maliyetinin olmaması ve Linux sunucuda sorunsuz çalışması belirleyici oldu.
- SeçilenEF CoreYerineDapper
İş yükünün neredeyse tamamı CRUD. Migration ve değişiklik takibinin hazır gelmesi, elle SQL yazarak kazanacağım milisaniyelerden daha değerliydi.
- SeçilenKatmanlı mimariYerineTek proje
İlk sürümü tek projede yazdım ve controller'lar 300 satırı geçmeye başladı. Katmanlara ayırınca hem test etmek hem de altı ay sonra okumak kolaylaştı.
- SeçilenDTOYerineVarlığı doğrudan döndürmek
Varlığı doğrudan döndürünce şemadaki her değişiklik API'yi kırıyordu. DTO araya girince şema ile sözleşme birbirinden bağımsız hale geldi.
Zorluklar
Satış anında stok düşümü
Satış kaydı ile stok düşümü iki ayrı yazma işlemi. İlki başarılı olup ikincisi patlarsa sistem tutarsız kalıyor. İkisini tek transaction'a aldım ve stok kontrolü ile düşüm arasına başka bir işlemin giremeyeceği şekilde sıraladım.
public async Task<Result> CreateSaleAsync(SaleDto dto)
{
await using var tx = await _context.Database.BeginTransactionAsync();
var stock = await _context.Stocks
.FirstOrDefaultAsync(s => s.ProductId == dto.ProductId);
if (stock is null || stock.Quantity < dto.Quantity)
return Result.Fail("Yetersiz stok");
stock.Quantity -= dto.Quantity;
_context.Sales.Add(dto.ToEntity());
await _context.SaveChangesAsync();
await tx.CommitAsync();
return Result.Ok();
}Listeleme sayfasındaki N+1
Satış listesinde her satır için ürün ve müşteri adı ayrı sorguyla çekiliyordu; 200 satırlık sayfa 400'den fazla sorgu üretiyordu. Include yerine doğrudan DTO'ya projeksiyon yaparak tek sorguya indirdim — üstelik gereksiz kolonları da artık taşımıyoruz.
// Önce: her satır için ayrı sorgu
// Sonra: tek sorgu, yalnızca gereken kolonlar
return await _context.Sales
.AsNoTracking()
.OrderByDescending(s => s.CreatedAt)
.Select(s => new SaleListItemDto
{
Id = s.Id,
ProductName = s.Product.Name,
CustomerName = s.Customer.FullName,
Quantity = s.Quantity,
Total = s.UnitPrice * s.Quantity
})
.ToListAsync();Sayılarla
- 0
- Çekirdek varlık
- 0
- Mimari katman
- 0
- Satış = tek transaction
Çıkarımlar
Katman ayrımının asıl faydası test değil, okunabilirlik. Bir hatayı ararken artık hangi dosyaya bakacağımı düşünmüyorum.
ORM'yi yavaşlıkla suçlamadan önce ürettiği SQL'e bakmak gerekiyor. Sorun neredeyse her zaman benim sorgumdaydı.