{"record":{"id":"e84acdcdfd3a2e63","repo":"dotnet/eShop","slug":"empty-stock-product-item-name-is-sold-out","errorCode":null,"errorMessage":"Empty stock, product item {Name} is sold out","messagePattern":"Empty stock, product item (.+?) is sold out","errorType":"exception","errorClass":"CatalogDomainException","httpStatus":null,"severity":"error","filePath":"src/Catalog.API/Model/CatalogItem.cs","lineNumber":66,"sourceCode":"\n\n    /// <summary>\n    /// Decrements the quantity of a particular item in inventory and ensures the restockThreshold hasn't\n    /// been breached. If so, a RestockRequest is generated in CheckThreshold. \n    /// \n    /// If there is sufficient stock of an item, then the integer returned at the end of this call should be the same as quantityDesired. \n    /// In the event that there is not sufficient stock available, the method will remove whatever stock is available and return that quantity to the client.\n    /// In this case, it is the responsibility of the client to determine if the amount that is returned is the same as quantityDesired.\n    /// It is invalid to pass in a negative number. \n    /// </summary>\n    /// <param name=\"quantityDesired\"></param>\n    /// <returns>int: Returns the number actually removed from stock. </returns>\n    /// \n    public int RemoveStock(int quantityDesired)\n    {\n        if (AvailableStock == 0)\n        {\n            throw new CatalogDomainException($\"Empty stock, product item {Name} is sold out\");\n        }\n\n        if (quantityDesired <= 0)\n        {\n            throw new CatalogDomainException($\"Item units desired should be greater than zero\");\n        }\n\n        int removed = Math.Min(quantityDesired, this.AvailableStock);\n\n        this.AvailableStock -= removed;\n\n        return removed;\n    }\n\n    /// <summary>\n    /// Increments the quantity of a particular item in inventory.\n    /// <param name=\"quantity\"></param>\n    /// <returns>int: Returns the quantity that has been added to stock</returns>","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/dotnet/eShop/blob/9b4f9434f46fdc5c1a6e9e936af2868340cdbc48/src/Catalog.API/Model/CatalogItem.cs#L48-L84","documentation":"Thrown by CatalogItem.RemoveStock when AvailableStock is exactly zero. RemoveStock is the only sanctioned way to decrement catalog inventory (DDD aggregate behavior); it refuses to operate on a sold-out item rather than returning a partial/zero result. It is a CatalogDomainException, surfaced to callers as a domain rule violation.","triggerScenarios":"A checkout/stock-reservation flow calls RemoveStock(quantityDesired) for an item whose AvailableStock has already been driven to 0 by prior orders. Also triggered by race conditions where two concurrent reservations drain the last unit, or by stale catalog data read before a previous decrement committed.","commonSituations":"High-demand/SKU product selling out under load; catalog cache returning an outdated positive stock figure; ordering service processing a backlogged queue after the catalog was depleted; integration tests not reseeding stock between runs.","solutions":["Before calling RemoveStock, check AvailableStock and short-circuit or surface an out-of-stock message to the user when it is 0.","Treat the returned int as authoritative: RemoveStock may return fewer units than requested when stock is partial — if it returns 0, branch to out-of-stock handling instead of retrying blindly.","Use optimistic concurrency (a row version / ETag on CatalogItem) to prevent two writers racing the last unit, and re-read stock on a concurrency conflict before retrying.","Restock via AddStock so AvailableStock is non-zero before further RemoveStock calls."],"exampleFix":"// before\nvar taken = item.RemoveStock(qty);\n\n// after\nif (item.AvailableStock == 0) {\n    return Result.OutOfStock(item.Name);\n}\nvar taken = item.RemoveStock(qty);\nif (taken < qty) {\n    return Result.PartiallyFulfilled(taken);\n}","handlingStrategy":"validation","validationCode":"if (item.AvailableStock <= 0) {\n    return Result.Fail($\"{item.Name} is sold out\");\n}\nvar taken = item.RemoveStock(qty);","typeGuard":"static bool HasStock(CatalogItem item) => item.AvailableStock > 0;","tryCatchPattern":"try {\n    var taken = item.RemoveStock(qty);\n} catch (CatalogDomainException ex) when (ex.Message.Contains(\"sold out\")) {\n    // surface out-of-stock to the user; do not retry blindly\n}","preventionTips":["Read AvailableStock fresh (no stale cache) before reserving.","Use optimistic concurrency on CatalogItem to serialize last-unit races.","Treat the returned int as authoritative and handle partial fulfillment."],"tags":["domain","catalog-api","inventory","concurrency","ddd"],"backgroundTag":null,"analyzedSha":"9b4f9434f46fdc5c1a6e9e936af2868340cdbc48","analyzedAt":"2026-08-13T19:29:36.594Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}