History and context
How fragmentation happens
A company rarely designs five sources of truth. It accumulates them. The online store owns product data, POS stores another price list, warehouse staff maintain stock separately, and administration tools add another configuration layer. Each application is useful, but the combined system becomes expensive to reconcile.
Define domain ownership first
| Domain | Owns |
|---|---|
| Catalog | Products, categories, attributes, sellability |
| Pricing | Base prices, promotions, eligibility rules |
| Inventory | Balances, reservations, movements |
| Orders | Order lifecycle, lines, totals |
| Fulfilment | Picking, preparation, dispatch |
| Identity | Users, roles and permissions |
Once ownership is explicit, APIs become much easier to design because each endpoint has a domain reason to exist.
One price engine, many channels
Promotions should not be reimplemented in each frontend.
public PriceResult Calculate(
IReadOnlyCollection<CartLine> lines,
IReadOnlyCollection<SalesOffer> offers)
{
var result = PriceResult.From(lines);
ApplyProductOffers(result, offers);
ApplyCombinedTargetOffers(result, offers);
ApplyOrderOffers(result, offers);
return result;
}The website, POS and admin quotation screens can all call this same capability. A campaign configured once therefore behaves consistently across channels.
Inventory is a ledger, not a number
Real inventory usually distinguishes on-hand, reserved, available, damaged, in-transfer and allocated quantities. A movement ledger preserves why stock changed.
CREATE TABLE inventory_movement (
movement_id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL,
location_id BIGINT NOT NULL,
movement_type VARCHAR(30) NOT NULL,
quantity_delta NUMERIC(18,3) NOT NULL,
reference_type VARCHAR(30),
reference_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Reliable checkout and the outbox pattern
Checkout should secure the critical business transaction first. Email, CRM sync and non-critical reporting should happen afterwards.
public sealed class OutboxMessage
{
public Guid Id { get; init; }
public DateTimeOffset OccurredAt { get; init; }
public string Type { get; init; } = default!;
public string Payload { get; init; } = default!;
public DateTimeOffset? ProcessedAt { get; set; }
}Real-time operations without making WebSockets the database
Kitchen screens, station boards and fulfilment dashboards benefit from server-pushed updates. ASP.NET Core SignalR is designed for this class of real-time web functionality. The client should still refetch authoritative state after reconnecting, because connection events are notifications rather than durable truth.
await hubContext.Clients
.Group($"shop:{shopId}:station:{stationId}")
.SendAsync("taskUpdated", task, cancellationToken);connection.on("taskUpdated", task => {
setTasks(current =>
current.map(x => x.id === task.id ? task : x)
);
});Menu visibility is not security
Frontend permission checks improve usability, but API authorisation remains authoritative.
[Authorize(Policy = "ORDER_REFUND")]
[HttpPost("orders/{orderId:long}/refund")]
public Task<IActionResult> Refund(long orderId)
{
// Server-side permission enforcement.
}Business value and practical considerations
Benefits
- One commercial model
- One order lifecycle
- Better reporting
- Lower reconciliation effort
- True omnichannel workflows
Practical considerations
- Shared platform becomes critical infrastructure
- Domain boundaries require discipline
- Migration must be staged carefully
- Operational monitoring matters more
References
Primary documentation used for terminology and current platform guidance: