Stok Takip Sistemi
A layered Web API that brings product, stock, sales, customer and supplier flows under one roof.
- Role
- Timeline
- Stack

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.
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.
Architecture
- 01
API
Thin controllers. HTTP, validation and DTO mapping only.
- Controllers
- DTO
- Validation
- Swagger
- 02
Business
All business rules live here. This layer knows nothing about HTTP.
- Services
- Rules
- Mapping
- 03
Data Access
A single entry point to data through EF Core.
- Repository
- DbContext
- Migrations
- 04
Domain
Entities and enums. Depends on nothing.
- Product
- Stock
- Sale
- Customer
- Supplier
- 05
PostgreSQL
Relational schema, foreign keys, indexes.
- Tables
- FK
- Index
Technical decisions
- ChosePostgreSQLOverMS 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 CoreOverDapper
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 architectureOverA 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.
- ChoseDTOOverReturning entities directly
Returning entities meant every schema change broke the API. With a DTO in between, schema and contract became independent.
Challenges
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.
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();
}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.
// Ö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();By the numbers
- 0
- Core entities
- 0
- Architecture layers
- 0
- Sale = one transaction
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.