Blog
Mert Özen's blog posts on software development, backend, frontend, and DevOps.
Liskov Substitution Principle: Behavioral Subtyping and NotSupportedException in .NET
LSP requires a subtype to satisfy not just the compiler but the base type's contract: preconditions aren't strengthened, postconditions aren't weakened, invariants and the history constraint are preserved. Three real violations in Bookstore's stock module, the NotSupportedException that T[] and ReadOnlyCollection throw from IList<T>.Add, capability queries with Stream.CanSeek, and verifying the contract against every implementation with an abstract xUnit test class.
Open/Closed Principle: Extending Without Modifying and the Speculative Generality Trap
OCP means adding new behavior as a new implementation without opening existing, working code; but any code can only be closed against a particular axis of change. Closing a growing notification switch with INotificationChannel and .NET 8 keyed services, HttpClient's DelegatingHandler chain, lightweight extension points with Func, and the cost of speculative generality.
Single Responsibility Principle: Not "One Thing" but One Actor
SRP does not mean "a class does one thing"; in Martin's definition a module should be responsible to one actor, the single person or group that requests the same change. An Employee example where an accounting change breaks the HR report, sharing data while separating behavior, a Facade, .NET's Stream/StreamWriter/Encoding split and the fragmentation trap.
Coupling and Cohesion: Two Measures of Good Design, Connascence and the Law of Demeter
Good design means weak links between parts (coupling) and strong unity inside each part (cohesion); connascence grades those links by kind, and the Law of Demeter limits how far objects reach into each other. Kinds of coupling and levels of cohesion, connascence of position in a six-parameter constructor, the TimeSpan(10) trap, and protecting a wallet balance with Tell, Don't Ask.
Why Design Principles Exist: Change, Dependencies and a Short History of SOLID
Design principles exist to name the unplanned dependencies that make change expensive and to draw boundaries around them: rigidity, fragility, immobility and viscosity share dependency as their root. The history from Parnas's information hiding to Feathers's SOLID acronym, and the series' Bookstore order and notification example.
Bulk Import with a Validation Report: Upload Excel/CSV, Staging Table, Row-Level Error File
The safe way to bring in a large, dirty Excel/CSV file is to load it into a text-column staging table with SqlBulkCopy, apply the rules to every row with set-based SQL, give the user a row-level error file and make the commit a separate, confirmed step. This post walks through a 20,000-row import with real output, including 202 Accepted plus a status endpoint, idempotency via a file hash and escaping against CSV injection.
Pagination Strategies in Depth: Offset, Keyset and Opaque Cursor Tokens
On large, changing tables keyset pagination removes offset's depth-dependent cost and page drift: in a 2-million-row test, page 5000 read 375 logical pages with offset and 6 with keyset. This post shows, with real measurements, the tie-breaker keyset condition on SQL Server, an HMAC-signed opaque cursor, the previous page and the nullable sort column trap.
Optimistic Concurrency End to End: From rowversion to ETag, If-Match, 412 and a Conflict UI
Building optimistic concurrency end to end on the web means exposing rowversion as an ETag, requiring If-Match on updates (428 otherwise), returning 412 with the current values on a mismatch and doing a field-level three-way merge on the client. As a follow-up to the EF Core Concurrency post, this post shows the HTTP and Angular side with real output.
Idempotency Keys: Not Creating the Same Order Twice on Double Clicks and Retries
The reliable way to stop the same order from being created twice on double clicks and retries is for the server to store a client-generated Idempotency-Key per user together with a request hash, and to write the work and the 'completed' record in the same transaction. This post covers the current status of the IETF draft (draft-08, not an RFC), the 400/409/422 rules and an ASP.NET Core endpoint filter + SQL Server implementation with real output.
Time and Time Zones: UTC Instants, Local Business Dates, DateOnly and TimeProvider
Storing time correctly means separating three concepts: event moments are UTC instants (DateTimeOffset), calendar days are DateOnly, and future local events are wall-clock time plus an IANA zone id. This post shows, with real output, how to turn a tenant's day into a UTC range, a testable clock with TimeProvider, the AT TIME ZONE traps in SQL Server and the date shift in Angular.
Money and Rounding: decimal, a Money Value Object, VAT and Distributing Cents
Modeling money correctly takes a Money value object that carries the amount with its currency, the same explicit rounding rule in C# and SQL, and Allocate instead of division so no cent is lost. The post shows Math.Round and SQL ROUND disagreeing on 0.125, the 1-cent difference between per-line and total VAT, an effective-dated VAT rate table (20%, 10%, 1% as of November 2026), EF Core complex type mapping and string amounts in JSON, with real output.
Numbering: Invoice and Order Numbers, Gapless Sequences and Yearly Resets
An invoice number that must be consecutive shouldn't come from an identity, MAX + 1 or a SEQUENCE, but from a locked UPDATE on a counter table keyed by (Series, Year) inside the approval transaction; for order numbers where gaps are acceptable, a SEQUENCE is enough. The post shows GİB's 16-character document number format, gapless numbering under 50 concurrent approvals, the effect of lock duration on throughput and yearly resets by invoice date, with real output.