Every second article about CQRS either presents it as the way to structure an application or as an over-engineering trap. Both are wrong in the same way: CQRS is not an architecture, it is a response to a specific pressure — a read path and a write path that want different shapes. A marketplace has exactly two places with that pressure. This is what we did with them in a .NET 10 build, and what we deliberately did not do with everything else.
The short answer
We used CQRS for product search (a read model denormalised for filters, updated from product and inventory events) and for order settlement (a write model that records per-vendor splits as immutable entries, read through projections). The vendor panel, admin, accounts and CMS stayed plain: one model, EF Core, straightforward services. About 15% of the codebase is CQRS; it handles about 80% of the traffic.
Problem one: search wants a shape the write model hates
A product in the write model is a normalised graph — product, variants, vendor, category tree, attribute values, stock per warehouse. Searching over that graph with five filters and a sort is a join party, and it gets slower with every vendor. The read model is a flat document per sellable variant: denormalised name, category path, attribute map, price, stock flag, vendor rating. Filters become predicates on one table (or one index); sorting is a column.
The read model is rebuilt from events: ProductUpdated, StockChanged, VendorRatingRecalculated. The handler is idempotent and the projection can be dropped and replayed — which is also how we migrated the search index twice without downtime.
What it cost: an eventual-consistency window of a few hundred milliseconds between a vendor saving a product and the change appearing in search. For a marketplace that is fine; for an inventory system it might not be, and that question is the whole decision.
Problem two: settlement wants an audit trail, not a balance
Settlement is where money is, so it is where mistakes cost. The naive model keeps a VendorBalance column and updates it. The moment you need to answer "why is this vendor's balance 4,120,000 and not 4,300,000", a column cannot tell you.
So the write model records entries: OrderItemCaptured(vendor, amount, commission), RefundIssued, SettlementPaid. Entries are appended, never updated. The vendor's balance is a projection — a sum — and so is the settlement statement, and so is the reconciliation report the finance team runs against the bank file. When the commission rule changed mid-year, we replayed the projection with the new rule for the affected period and could show both figures side by side.
What it cost: more tables, more code, and a team that had to learn to think in entries. Worth it for money. Not worth it for a vendor's shop description.
Where we left CQRS out, on purpose
- Vendor panel and admin — CRUD over the normalised model with EF Core. Traffic is low, consistency must be immediate, and the shape read and written is the same.
- Customer accounts — same.
- Content — pages, banners, categories: a CMS problem, not a domain problem.
The temptation is symmetry — "if search has a read model, everything should". Resist it. Every read model is a second copy of truth you have to keep honest.
Practical notes for .NET 10
- MediatR is enough for commands and queries; we did not need a bus until the projection rebuilds, where a simple outbox table and a hosted service did the job.
- Keep projections in the same database at first. A separate search engine is a later optimisation with real operational cost; PostgreSQL with a GIN index on the attribute map carried tens of thousands of SKUs comfortably.
- Test the settlement projection with property-based tests: for any sequence of entries, the sum of vendor balances plus commissions must equal captured revenue. That invariant found two bugs before production.
If you are scoping a marketplace and want to see this shape before committing, the marketplace development page describes what we deliver; the settlement model above is part of it.





Comments
No comments yet. Be the first.