What Is Bookstore? The Shared Example Application Behind This Site's Series: Product, Users and Business Rules
Bookstore is an online bookstore chosen so that every series on the site takes place in the same world: retail sales, Institutional Orders for schools and libraries, and a back office. We cover its six roles, the main flow from catalog to shipping, business rules such as the 20-copy minimum, the 10 percent institutional discount, non-stacking campaigns and rounding half a cent up, the product's numbers and its team.
Version note: The code in this post comes from the series' reference application and was built and run with C# 14 on .NET 10.
Guess First: Three Carts, Three Outcomes
Bookstore, the online bookstore used as the shared example across this site's series, receives three orders on the same day. The prices come from the reference application's seed data: Dune 489.00 TRY, Emma 249.90 TRY, Pride and Prejudice 239.90 TRY, Frankenstein 219.90 TRY, The Hobbit 329.90 TRY.
- Ada, an individual customer, buys 2 Dune and 1 Emma.
- Nevin Hanım orders 12 Pride and Prejudice and 7 Frankenstein for the school she works for.
- A city library wants 21 copies of The Hobbit; The Hobbit is on a 25 percent campaign that week.
Two questions for each order: is it accepted, and if so, what is the total? For the third one, which discount does the library get: 25 percent, the 10 percent institutional discount, or both added together? The answers are in the business rules section. Before you get there, write your own numbers down, and watch the last cent of the third order.
Why a Single Example Application?
The site has hundreds of posts and dozens of series. For a while every series picked its own example: a product catalog in one, an HR portal in another, a generic shop in a third. Readers had to learn a new domain at the start of every series, and the actual topic, whether an EF Core migration, an Angular signal or a security hole, came second.
Bookstore fixes this by building one world. You learn the words Book, Order, Customer and StockItem once. Then you see their tables in the Data Modeling and Normalization series, their mappings in the Entity Framework Core series, their endpoints in the ASP.NET Core Web API series, their screens in the Angular series and their vulnerabilities in the Web Security (OWASP Top 10) series. Topics connect: the stock reservation discussed in one series becomes a table in another, then an endpoint and a test. When a post asks "why was this order saved twice?", you already know which table and which module to look at.
The idea is not new. Microsoft's dotnet/eShop repository describes itself in its README as "A reference .NET application implementing an eCommerce site". The difference: eShop shows one architecture end to end, while Bookstore grows the same product step by step through the eyes of different series. The Bookstore that is a monolith in one series is split into pieces in the Microservices series, and in the CQRS and Event Sourcing series its ordering side is rewritten with separate write and read models.
Why a bookstore? Everybody knows what a book, a cart and a parcel are, so no pages are spent explaining the domain. Yet the domain has real problems: rounding in money calculations, two people buying the last copy at the same moment, approval flows in institutions, traffic that grows tenfold when schools open. The examples stay simple, the problems stay real. The "What is Bookstore?" box on the site links to this series too; whichever series you come from, this post gives the same answer to the same question.
The Product: What Does Bookstore Sell, and to Whom?
Bookstore sells books online to individual customers. There is a single seller: Bookstore itself. It is not a marketplace and does not list other sellers' products. The product has three faces.
Retail sales
Catalog and search, book details, cart, order, payment, shipping, returns, campaigns and discounts. The customer finds a book, adds it to the cart and places the order; the payment is captured, the book is shipped and the customer gets an e-mail. Prices are kept as decimal in TRY (or EUR when needed) and times are stored in UTC. An ISBN-13 can identify a book, and the URLs of the reference application use the ISBN as well.
Institutional Orders
Bulk orders from schools, libraries and companies, the B2B side of the product. The feature was worked out in the Requirements Analysis series, starting from the problem behind Hakan's request for an "Excel upload screen". The rules that came out of it: an order contains at least 20 copies in total, the institutional discount is 10 percent, and the option of a partial shipment when stock runs short was left to Release 2.
An institutional customer has its own units, such as campuses, branches and departments, and its own users; the units form a hierarchy and purchases go through an approval flow. That is why multi-tenant and hierarchical data scenarios are always told through these institutions on the site.
Back office
Goods receipts in the warehouse, purchase orders to suppliers, return approvals, price and campaign management, reports. The customer never sees this side, but it runs behind every order.
Users and Roles
Bookstore has six roles. The role names appear in the code exactly like this:
| Role | Who | What they do |
|---|---|---|
Customer | Individual customer | Browses the catalog, places orders, asks for returns |
InstitutionBuyer | The institution's buyer, for example Nevin Hanım | Prepares the bulk order |
InstitutionApprover | Person with approval rights in the institution | Approves or rejects the order |
StoreStaff | Store team | Prices, campaigns and return approvals |
WarehouseStaff | Warehouse team | Goods receipts and shipment preparation |
Admin | System administrator | Users and roles |
Roles live in the Identity module, and authentication and authorization are covered in the Authentication and Authorization series. The reference application has no sign-in yet: the cart is anonymous and an order is placed with a name and an e-mail address. That is a deliberate simplification; sign-in, roles and institution users are added in the related series.
The Main Flow: From Catalog to Shipping

On the happy path an order passes through six modules. A book is chosen in Catalog. Ordering turns the cart into an order and publishes the OrderPlaced event. Inventory reserves the stock (StockReserved); when stock is short the result is OutOfStock. Payments captures the money from the payment provider (PaymentCaptured or PaymentFailed). Shipping creates the shipping label and tracking number (OrderShipped). Notifications sends the order confirmation and shipping details to the customer by e-mail or SMS.
Two decisions in the flow come from the Introduction to Architecture series. First, if the payment provider does not answer within 5 seconds, the order moves to PaymentPending and the user does not get an error. Second, the post-order e-mail is not sent synchronously but through a queue; at first an in-process Channel<T> is used and the risk of losing messages is accepted on purpose, while durability arrives with the outbox in the Messaging and Event-Driven Architecture series.
The side branches use the same modules. In an institutional order the InstitutionBuyer prepares the cart, the InstitutionApprover approves it, and only then does the order enter the normal flow. Returns are approved by StoreStaff; Payments refunds the money and Inventory takes the book back into stock. Campaigns are defined by StoreStaff as well, and when a price changes Catalog publishes BookPriceChanged.
Business Rules
Money and rounding
Money is decimal everywhere. Each line total is rounded to the cent on its own, with half a cent going up. This decision was also made with Bookstore's accounting team in the Requirements Analysis series. In the reference application the rule lives in a single method:
// Each line is rounded to two decimals, midpoint away from zero (Requirements Analysis series).
public static decimal CalculateLineTotal(decimal unitPrice, int quantity) =>
Math.Round(unitPrice * quantity, 2, MidpointRounding.AwayFromZero);
Called without a MidpointRounding argument, Math.Round in .NET uses banker's rounding, MidpointRounding.ToEven, which sends a midpoint value to the nearest even digit. Most days you will not notice the difference; on some invoices it shows up as one cent.
Discounts
The institutional discount is 10 percent. Campaigns are defined per book. Discounts do not stack: if both the institutional discount and a campaign apply to a line, the larger one wins. Hakan gave this answer in the Example Mapping session of the Requirements Analysis series.
Institutional minimum
An institutional order contains at least 20 copies in total. Twenty copies of one book count, and so do twenty copies split across two books. The rule looks at the order, not at the shipment: even if the first parcel of a partial shipment holds 12 copies, the order is valid.
Stock reservation
Stock is reserved the moment the order is placed. Inventory keeps the quantity on hand and the reserved quantity separately for every book; the sellable quantity is the difference. If stock is short, the order is rejected; in the reference application that is a 409 Conflict response. Letting an institution say "send the rest later" is Release 2's job.
Order states
public enum OrderStatus
{
Placed,
PaymentPending,
Paid,
Shipped,
Delivered,
Cancelled
}
An order is born as Placed. It becomes PaymentPending if the payment provider is late and Paid once the money is captured; Shipped when it is handed to the carrier, Delivered when it arrives. Cancelled is for orders cancelled before shipping. Today the reference application only produces Placed; the other transitions are written by the series that add the payment and shipping modules.
Answers to Guess First
Ada's order is accepted: 2 × 489.00 = 978.00 and 1 × 249.90 = 249.90, 1,227.90 TRY in total. The reference application's API returns this for the same cart:
{"id":2,"status":"Placed","placedAtUtc":"2026-11-14T00:42:59.331223Z","items":[{"title":"Dune","unitPrice":489.00,"quantity":2,"lineTotal":978.00},{"title":"Emma","unitPrice":249.90,"quantity":1,"lineTotal":249.90}],"total":1227.90,"currency":"TRY"}
Nevin Hanım's order is rejected: 12 + 7 = 19 copies, one short of the institutional minimum. With one more Frankenstein the order goes through and both lines get 10 percent off.
The library's order is accepted. The list total is 21 × 329.90 = 6,927.90 TRY. The 10 percent institutional discount and the 25 percent campaign do not stack; the larger one, 25 percent, applies. 6,927.90 × 0.75 = 5,195.925, and rounding half a cent up gives a line total of 5,195.93 TRY. Anyone who adds the discounts to 32.5 percent, or who gets 5,195.92 with the default Math.Round, issues the wrong invoice. With only 10 percent the amount would have been 6,235.11 TRY.
The Numbers: How Big Is Bookstore?
The architecture series base their decisions on these numbers. The catalog holds about 40,000 books. About 15,000 unique visitors come every day and about 800 orders are placed. During the back-to-school and year-end campaigns traffic grows to about ten times normal: at the peak about 120 orders per minute, and about 200 requests per second on catalog search.
Measurable targets were set in the Introduction to Architecture series: catalog search p95 under 300 ms at 200 requests per second, checkout p95 under 800 ms, and 99.9 percent monthly availability for checkout. The eighteen-month goal is to triple the order volume, open in a second country with an English interface and ship a mobile app that uses the same API.
Today's team is three developers and a part-time ops person, deploying once a week. Within twelve to eighteen months it is expected to grow into four stream-aligned teams, Catalog and search, Ordering and payment, Fulfillment covering Inventory and Shipping, and Customer Experience covering Notifications and accounts, plus one platform team. These are the product's numbers, not the 20-book seed data of the reference application.
The Team: Selin, Emre, Ayşe, Hakan, Gül and Nevin Hanım
In the process and project management series a team builds Bookstore. Selin is the Product Owner. Emre and Ayşe are developers. Hakan is responsible for institutional sales and has worked with schools for years. Gül works in customer service and is the person who types in the lists sent by institutions by hand today. Nevin Hanım is not part of the team: she is a school's purchasing officer, the institutional customer herself.
The six appear together most in the Requirements Analysis series: Hakan's request, Gül's correction list, the interview with Nevin Hanım and Selin's prioritization decisions are all there. In the Scrum series Selin, Ayşe and Emre are joined by Zeynep on the Angular side, Can on the pipeline, Deniz on test automation and Kerem as Scrum Master. In code examples people are called Ada, Alan, Grace, Linus and similar international names, and the books are well-known classics such as Dune, Emma, The Hobbit and Middlemarch.
Common Pitfalls
Taking the reference application for the whole product. The main branch of the repo contains only Catalog, Ordering and Inventory. Payments, Shipping, Notifications, Institutional and Identity are added on the branches of the series that explain them. The answer to "where is the payment code?" is not on main.
Applying the product's numbers to the reference application. A search over a 20-book database stays far below the 300 ms target; that says nothing about 40,000 books at 200 requests per second. The performance series measure with a realistic data volume.
Treating the rules as universal truths. At least 20 copies and a 10 percent discount are Bookstore's example decisions; the institutional rule in your product may be different. The series keep these rules fixed so that a result you read in one series still holds in another.
Getting lost when an older post shows a different example. Some older posts still use an HR portal or a generic product catalog. The mapping is clear: company, department and employee are Institution, InstitutionUnit and InstitutionUser in Bookstore; a leave or purchase approval is PurchaseApproval; ProductCatalog and Product are Catalog and Book.
Leaving rounding to the default. Math.Round(x, 2) and Math.Round(x, 2, MidpointRounding.AwayFromZero) can give different results; in the library example the difference was one cent.
A Small Experiment
★☆☆ What would the line total be if the library bought 20 copies of The Hobbit instead of 21? Is there a difference between the default Math.Round and AwayFromZero this time, and why?
★★☆ Nevin Hanım renews her order as 12 Pride and Prejudice and 8 Frankenstein; Pride and Prejudice is on a 15 percent campaign that week. Which discount applies to each line, and what are the line totals and the order total? Check the calculation with the CalculateLineTotal logic in a small console application.
★★★ For every step of the flow in the figure, write down which role starts or triggers it. Which module should own the institutional approval (PurchaseApproval): Ordering or Institutional? Write your decision with a one-sentence justification.
In this post we looked at Bookstore's product side: what it sells, to whom, and under which rules. In the next post we move to its technical face: the modules and their events, BookstoreContext and the SQL Server schema, the Web API endpoints and the Angular client. We will run the reference application on the repo's main branch step by step on Windows.