Metin Kayaalp
000Loading
Skip to content
Metin Kayaalp
← Back to work
012025

Stok Takip Sistemi

A layered Web API that brings product, stock, sales, customer and supplier flows under one roof.

Role
Backend — solo developer
Timeline
2025 · ~8 weeks
Stack
.NET 9 · ASP.NET Core · EF Core · PostgreSQL · REST
Stok Takip Sistemi
01

Problem

Most small businesses still keep stock in a spreadsheet. The sales record lives in one place and the stock count in another, and the link between them is maintained by hand. The result is always the same: products that exist on paper but not on the shelf.

02

Goal

Put sales and stock inside a single transaction, move business rules out of controllers into their own layer, and separate the database schema from the API contract.

03

Architecture

The layers and the boundaries between them.

  1. 01

    API

    Thin controllers. HTTP, validation and DTO mapping only.

    • Controllers
    • DTO
    • Validation
    • Swagger
  2. 02

    Business

    All business rules live here. This layer knows nothing about HTTP.

    • Services
    • Rules
    • Mapping
  3. 03

    Data Access

    A single entry point to data through EF Core.

    • Repository
    • DbContext
    • Migrations
  4. 04

    Domain

    Entities and enums. Depends on nothing.

    • Product
    • Stock
    • Sale
    • Customer
    • Supplier
  5. 05

    PostgreSQL

    Relational schema, foreign keys, indexes.

    • Tables
    • FK
    • Index
04

Technical decisions

What I picked over what, and why.

  • ChosePostgreSQL
    OverMS SQL Server

    I had been using MS SQL, but this project was going to run at a business. Having no licence cost and running cleanly on a Linux server decided it.

  • ChoseEF Core
    OverDapper

    Almost the entire workload is CRUD. Getting migrations and change tracking for free was worth more than the milliseconds I would win writing SQL by hand.

  • ChoseLayered architecture
    OverA single project

    I wrote the first version as a single project and controllers started passing 300 lines. Splitting into layers made it easier to test and easier to read six months later.

  • ChoseDTO
    OverReturning entities directly

    Returning entities meant every schema change broke the API. With a DTO in between, schema and contract became independent.

05

Challenges

Where I got stuck and how I got out.

01

Decrementing stock at sale time

Recording a sale and decrementing stock are two separate writes. If the first succeeds and the second fails, the system is left inconsistent. I put both in one transaction and ordered it so no other operation can slip between the availability check and the decrement.

SaleService.cscsharp
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();
}
02

The N+1 on the listing page

On the sales list, the product and customer name for each row were fetched with separate queries; a 200-row page produced more than 400 queries. Instead of Include I projected straight into the DTO, bringing it down to a single query — and we no longer carry unused columns.

SaleRepository.cscsharp
// Ö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();
06

By the numbers

0
Core entities
0
Architecture layers
0
Sale = one transaction
07

Takeaways

  • The real benefit of layering is not testing, it is readability. When hunting a bug I no longer wonder which file to open.

  • Before blaming the ORM for being slow, look at the SQL it generates. The problem was almost always my query.

Next project

Türk İşaret Dili Çevirisi

↗