Chapter 07: Domain Model
Bounded Contexts and Entity Definitions
This chapter defines the complete domain model for the POS Platform. We organize the system into six bounded contexts, each with clear responsibilities and well-defined entities.
Bounded Contexts Overview
Domain Bounded Contexts
=======================
+------------------------------------------------------------------+
| POS PLATFORM |
| |
| +-------------+ +-------------+ +-------------+ |
| | CATALOG | | SALES | | INVENTORY | |
| | | | | | | |
| | Products | | Sales | | StockLevels | |
| | Variants | | LineItems | | Adjustments | |
| | Categories | | Payments | | Transfers | |
| | Pricing | | Refunds | | Counts | |
| +-------------+ +-------------+ +-------------+ |
| |
| +-------------+ +-------------+ +-------------+ |
| | CUSTOMER | | EMPLOYEE | | LOCATION | |
| | | | | | | |
| | Customers | | Employees | | Locations | |
| | Addresses | | Roles | | Registers | |
| | Loyalty | | Permissions | | Settings | |
| | Credits | | Shifts | | TaxRates | |
| +-------------+ +-------------+ +-------------+ |
| |
+------------------------------------------------------------------+
Context 1: Catalog
The Catalog context manages all product-related data.
Entity: Product
+------------------------------------------------------------------+
| PRODUCT |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| sku | String(50) | Unique stock keeping unit |
| barcode | String(50) | UPC/EAN barcode (nullable) |
| name | String(255) | Display name |
| description | Text | Full description |
| category_id | UUID | FK to Category |
| brand | String(100) | Brand name |
| vendor | String(100) | Supplier/vendor name |
| cost | Decimal | Wholesale cost |
| price | Decimal | Retail price |
| compare_at_price| Decimal | Original price (for discounts) |
| tax_code | String(20) | Tax category code |
| is_taxable | Boolean | Subject to sales tax |
| track_inventory | Boolean | Enable inventory tracking |
| is_active | Boolean | Available for sale |
| shopify_id | String(50) | Shopify product ID (if synced) |
| image_url | String(500) | Primary product image |
| weight | Decimal | Weight in default unit |
| weight_unit | String(10) | lb, kg, oz, g |
| tags | String[] | Searchable tags |
| metadata | JSONB | Custom attributes |
| created_at | Timestamp | Creation timestamp |
| updated_at | Timestamp | Last update timestamp |
+------------------------------------------------------------------+
Entity: ProductVariant
+------------------------------------------------------------------+
| PRODUCT_VARIANT |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| product_id | UUID | FK to Product (required) |
| sku | String(50) | Unique variant SKU |
| barcode | String(50) | Variant barcode |
| name | String(255) | Variant name (e.g., "Large/Blue") |
| option1_name | String(50) | First option name (e.g., "Size") |
| option1_value | String(100) | First option value (e.g., "Large")|
| option2_name | String(50) | Second option name |
| option2_value | String(100) | Second option value |
| option3_name | String(50) | Third option name |
| option3_value | String(100) | Third option value |
| cost | Decimal | Variant cost (overrides product) |
| price | Decimal | Variant price (overrides product) |
| weight | Decimal | Variant weight |
| image_url | String(500) | Variant-specific image |
| shopify_variant_id | String(50) | Shopify variant ID |
| is_active | Boolean | Available for sale |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: Category
+------------------------------------------------------------------+
| CATEGORY |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| name | String(255) | Category name |
| slug | String(255) | URL-friendly identifier |
| parent_id | UUID | FK to parent Category (nullable) |
| description | Text | Category description |
| image_url | String(500) | Category image |
| sort_order | Integer | Display order |
| is_active | Boolean | Show in UI |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: PricingRule
+------------------------------------------------------------------+
| PRICING_RULE |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| name | String(255) | Rule name |
| type | String(50) | percentage, fixed, buy_x_get_y |
| value | Decimal | Discount value or percentage |
| product_id | UUID | Apply to specific product |
| category_id | UUID | Apply to category |
| customer_group | String(50) | Apply to customer group |
| min_quantity | Integer | Minimum quantity required |
| start_date | Timestamp | Rule start date |
| end_date | Timestamp | Rule end date |
| priority | Integer | Rule priority (higher wins) |
| is_active | Boolean | Rule is enabled |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Context 2: Sales
The Sales context handles all transaction-related data.
Entity: Sale
+------------------------------------------------------------------+
| SALE |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| sale_number | String(50) | Human-readable sale ID |
| location_id | UUID | FK to Location (required) |
| register_id | String(20) | Register identifier |
| employee_id | UUID | FK to Employee (cashier) |
| customer_id | UUID | FK to Customer (nullable) |
| status | String(20) | draft, completed, voided, refunded|
| subtotal | Decimal | Sum of line items before tax |
| discount_total | Decimal | Total discounts applied |
| tax_total | Decimal | Total tax amount |
| total | Decimal | Final total (subtotal-discount+tax)|
| payment_status | String(20) | pending, partial, paid, refunded |
| source | String(20) | pos, online, mobile |
| notes | Text | Sale notes |
| voided_at | Timestamp | When sale was voided |
| voided_by | UUID | Employee who voided |
| void_reason | Text | Reason for void |
| created_at | Timestamp | Sale timestamp |
| updated_at | Timestamp | Last update |
+------------------------------------------------------------------+
Entity: SaleLineItem
+------------------------------------------------------------------+
| SALE_LINE_ITEM |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| sale_id | UUID | FK to Sale (required) |
| product_id | UUID | FK to Product |
| variant_id | UUID | FK to ProductVariant |
| sku | String(50) | SKU at time of sale |
| name | String(255) | Product name at time of sale |
| quantity | Integer | Quantity sold |
| unit_price | Decimal | Price per unit |
| unit_cost | Decimal | Cost per unit (for profit calc) |
| discount_amount | Decimal | Discount on this line |
| discount_reason | String(100) | Reason for discount |
| tax_amount | Decimal | Tax on this line |
| total | Decimal | Line total |
| is_refunded | Boolean | Line was refunded |
| refunded_at | Timestamp | When refunded |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: Payment
+------------------------------------------------------------------+
| PAYMENT |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| sale_id | UUID | FK to Sale (required) |
| payment_method | String(50) | cash, credit, debit, gift, store_credit |
| amount | Decimal | Payment amount |
| tendered | Decimal | Amount tendered (for cash) |
| change_given | Decimal | Change returned |
| reference | String(100) | Card last 4, check #, etc. |
| card_type | String(20) | visa, mastercard, amex, discover |
| auth_code | String(50) | Authorization code |
| status | String(20) | pending, completed, failed, refunded |
| gateway_response| JSONB | Full payment gateway response |
| created_at | Timestamp | Payment timestamp |
+------------------------------------------------------------------+
Entity: Refund
+------------------------------------------------------------------+
| REFUND |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| sale_id | UUID | FK to original Sale |
| refund_number | String(50) | Human-readable refund ID |
| employee_id | UUID | FK to Employee (who processed) |
| reason | String(100) | Refund reason |
| subtotal | Decimal | Refund subtotal |
| tax_refunded | Decimal | Tax refunded |
| total | Decimal | Total refund amount |
| refund_method | String(50) | original, cash, store_credit |
| notes | Text | Additional notes |
| created_at | Timestamp | Refund timestamp |
+------------------------------------------------------------------+
Entity: RefundLineItem
+------------------------------------------------------------------+
| REFUND_LINE_ITEM |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| refund_id | UUID | FK to Refund |
| sale_line_item_id | UUID | FK to original SaleLineItem |
| quantity | Integer | Quantity refunded |
| amount | Decimal | Refund amount for this line |
| restock | Boolean | Add back to inventory |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Context 3: Inventory
The Inventory context manages stock levels and movements.
Entity: InventoryItem
+------------------------------------------------------------------+
| INVENTORY_ITEM |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| product_id | UUID | FK to Product |
| variant_id | UUID | FK to ProductVariant |
| location_id | UUID | FK to Location (required) |
| quantity_on_hand| Integer | Current stock quantity |
| quantity_committed | Integer | Reserved for pending orders |
| quantity_available | Integer | Calculated: on_hand - committed |
| quantity_incoming | Integer | Expected from purchase orders |
| reorder_point | Integer | Alert when below this level |
| reorder_quantity| Integer | Default reorder amount |
| bin_location | String(50) | Physical bin/shelf location |
| last_counted_at | Timestamp | Last physical count |
| last_received_at| Timestamp | Last inventory receipt |
| last_sold_at | Timestamp | Last sale of this item |
| created_at | Timestamp | Creation timestamp |
| updated_at | Timestamp | Last update |
+------------------------------------------------------------------+
Entity: InventoryAdjustment
+------------------------------------------------------------------+
| INVENTORY_ADJUSTMENT |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| adjustment_number | String(50)| Human-readable ID |
| location_id | UUID | FK to Location |
| employee_id | UUID | FK to Employee (who adjusted) |
| reason | String(50) | count, damage, theft, return, etc.|
| notes | Text | Adjustment notes |
| status | String(20) | draft, pending, completed |
| created_at | Timestamp | Adjustment timestamp |
| completed_at | Timestamp | When finalized |
+------------------------------------------------------------------+
Entity: InventoryAdjustmentItem
+------------------------------------------------------------------+
| INVENTORY_ADJUSTMENT_ITEM |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| adjustment_id | UUID | FK to InventoryAdjustment |
| product_id | UUID | FK to Product |
| variant_id | UUID | FK to ProductVariant |
| expected_quantity | Integer | Quantity before adjustment |
| actual_quantity | Integer | New quantity |
| quantity_change | Integer | Difference (can be negative) |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: InventoryTransfer
+------------------------------------------------------------------+
| INVENTORY_TRANSFER |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| transfer_number | String(50) | Human-readable ID |
| from_location_id| UUID | FK to source Location |
| to_location_id | UUID | FK to destination Location |
| employee_id | UUID | FK to Employee (initiator) |
| status | String(20) | draft, pending, in_transit, received |
| notes | Text | Transfer notes |
| shipped_at | Timestamp | When shipped |
| received_at | Timestamp | When received |
| received_by | UUID | FK to Employee (receiver) |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: InventoryTransferItem
+------------------------------------------------------------------+
| INVENTORY_TRANSFER_ITEM |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| transfer_id | UUID | FK to InventoryTransfer |
| product_id | UUID | FK to Product |
| variant_id | UUID | FK to ProductVariant |
| quantity_sent | Integer | Quantity shipped |
| quantity_received | Integer | Quantity actually received |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Context 4: Customer
The Customer context manages customer data and loyalty.
Entity: Customer
+------------------------------------------------------------------+
| CUSTOMER |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| customer_number | String(20) | Human-readable customer ID |
| first_name | String(100) | First name |
| last_name | String(100) | Last name |
| email | String(255) | Email address (unique) |
| phone | String(20) | Phone number |
| company | String(255) | Company name |
| date_of_birth | Date | Birthday (for loyalty) |
| tax_exempt | Boolean | Tax exempt status |
| tax_exempt_id | String(50) | Tax exemption certificate |
| notes | Text | Customer notes |
| loyalty_points | Integer | Current loyalty points |
| loyalty_tier | String(20) | bronze, silver, gold, platinum |
| total_spent | Decimal | Lifetime spending |
| visit_count | Integer | Total visits |
| average_order | Decimal | Average order value |
| last_visit_at | Timestamp | Last visit timestamp |
| tags | String[] | Customer tags |
| marketing_consent | Boolean | Opted in for marketing |
| shopify_id | String(50) | Shopify customer ID |
| created_at | Timestamp | Creation timestamp |
| updated_at | Timestamp | Last update |
+------------------------------------------------------------------+
Entity: CustomerAddress
+------------------------------------------------------------------+
| CUSTOMER_ADDRESS |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| customer_id | UUID | FK to Customer |
| address_type | String(20) | billing, shipping |
| is_default | Boolean | Default address for type |
| first_name | String(100) | Recipient first name |
| last_name | String(100) | Recipient last name |
| company | String(255) | Company name |
| address_line1 | String(255) | Street address line 1 |
| address_line2 | String(255) | Street address line 2 |
| city | String(100) | City |
| state | String(50) | State/Province |
| postal_code | String(20) | ZIP/Postal code |
| country | String(2) | Country code (ISO 3166-1) |
| phone | String(20) | Contact phone |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: StoreCredit
+------------------------------------------------------------------+
| STORE_CREDIT |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| customer_id | UUID | FK to Customer |
| code | String(50) | Unique credit code |
| original_amount | Decimal | Initial credit amount |
| current_balance | Decimal | Remaining balance |
| reason | String(100) | Reason for credit |
| issued_by | UUID | FK to Employee |
| expires_at | Timestamp | Expiration date (nullable) |
| is_active | Boolean | Credit is usable |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: LoyaltyTransaction
+------------------------------------------------------------------+
| LOYALTY_TRANSACTION |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| customer_id | UUID | FK to Customer |
| sale_id | UUID | FK to Sale (if earned from sale) |
| type | String(20) | earn, redeem, adjustment, expire |
| points | Integer | Points (positive or negative) |
| balance_after | Integer | Balance after transaction |
| description | String(255) | Transaction description |
| created_by | UUID | FK to Employee |
| created_at | Timestamp | Transaction timestamp |
+------------------------------------------------------------------+
Context 5: Employee
The Employee context manages users, roles, and shifts.
Entity: Employee
+------------------------------------------------------------------+
| EMPLOYEE |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| employee_number | String(20) | Human-readable employee ID |
| first_name | String(100) | First name |
| last_name | String(100) | Last name |
| email | String(255) | Email address (unique) |
| phone | String(20) | Phone number |
| pin_hash | String(255) | Hashed PIN for clock-in |
| role_id | UUID | FK to Role |
| home_location_id| UUID | FK to primary Location |
| hire_date | Date | Date of hire |
| termination_date| Date | Date of termination |
| hourly_rate | Decimal | Hourly pay rate |
| commission_rate | Decimal | Commission percentage |
| is_active | Boolean | Employee is active |
| last_login_at | Timestamp | Last login timestamp |
| created_at | Timestamp | Creation timestamp |
| updated_at | Timestamp | Last update |
+------------------------------------------------------------------+
Entity: Role
+------------------------------------------------------------------+
| ROLE |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| name | String(100) | Role name |
| code | String(50) | Role code (admin, manager, etc.) |
| description | Text | Role description |
| is_system | Boolean | System role (cannot delete) |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: Permission
+------------------------------------------------------------------+
| PERMISSION |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| code | String(100) | Permission code |
| name | String(255) | Permission name |
| category | String(50) | Grouping category |
| description | Text | What this permission allows |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: RolePermission
+------------------------------------------------------------------+
| ROLE_PERMISSION |
+------------------------------------------------------------------+
| role_id | UUID | FK to Role |
| permission_id | UUID | FK to Permission |
| created_at | Timestamp | When assigned |
| PRIMARY KEY (role_id, permission_id) |
+------------------------------------------------------------------+
Entity: Shift
+------------------------------------------------------------------+
| SHIFT |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| employee_id | UUID | FK to Employee |
| location_id | UUID | FK to Location |
| clock_in | Timestamp | Clock in time |
| clock_out | Timestamp | Clock out time |
| break_minutes | Integer | Total break time |
| notes | Text | Shift notes |
| status | String(20) | active, completed, edited |
| edited_by | UUID | FK to Employee (if edited) |
| edit_reason | Text | Reason for edit |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Context 6: Location
The Location context manages stores, registers, and settings.
Entity: Location
+------------------------------------------------------------------+
| LOCATION |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| code | String(10) | Short code (HQ, GM, LM) |
| name | String(255) | Full location name |
| type | String(20) | store, warehouse, popup |
| address_line1 | String(255) | Street address |
| address_line2 | String(255) | Suite/unit |
| city | String(100) | City |
| state | String(50) | State/Province |
| postal_code | String(20) | ZIP/Postal code |
| country | String(2) | Country code |
| phone | String(20) | Phone number |
| email | String(255) | Email address |
| timezone | String(50) | IANA timezone |
| currency | String(3) | Currency code |
| shopify_location_id | String(50) | Shopify location ID |
| is_active | Boolean | Location is operational |
| can_fulfill | Boolean | Can fulfill online orders |
| is_visible_online | Boolean | Show inventory online |
| fulfillment_priority | Integer| Order for fulfillment routing |
| opening_hours | JSONB | Weekly schedule |
| created_at | Timestamp | Creation timestamp |
| updated_at | Timestamp | Last update |
+------------------------------------------------------------------+
Entity: Register
+------------------------------------------------------------------+
| REGISTER |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| location_id | UUID | FK to Location |
| register_number | String(20) | Register identifier |
| name | String(100) | Display name |
| receipt_footer | Text | Custom receipt message |
| is_active | Boolean | Register is operational |
| last_opened_at | Timestamp | Last opened |
| last_closed_at | Timestamp | Last closed |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: CashDrawer
+------------------------------------------------------------------+
| CASH_DRAWER |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| register_id | UUID | FK to Register |
| employee_id | UUID | FK to Employee (opened by) |
| opened_at | Timestamp | When opened |
| closed_at | Timestamp | When closed |
| opening_balance | Decimal | Starting cash amount |
| closing_balance | Decimal | Ending cash amount |
| expected_balance| Decimal | Expected based on transactions |
| variance | Decimal | Difference (closing - expected) |
| notes | Text | Drawer notes |
| status | String(20) | open, closed, reconciled |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity: TaxRate
+------------------------------------------------------------------+
| TAX_RATE |
+------------------------------------------------------------------+
| id | UUID | Primary key |
| location_id | UUID | FK to Location (nullable=all) |
| name | String(100) | Tax name |
| rate | Decimal | Tax rate percentage |
| tax_code | String(20) | Tax category code |
| is_compound | Boolean | Calculated on tax-inclusive total |
| priority | Integer | Order of application |
| is_active | Boolean | Tax is active |
| created_at | Timestamp | Creation timestamp |
+------------------------------------------------------------------+
Entity Relationship Diagram
Entity Relationships
====================
+----------+
| Category |
+----+-----+
|
| 1:N
v
+----------+ 1:N +----------+ 1:N +----------------+
| Location |<-------------| Product |------------->| ProductVariant |
+----+-----+ +----+-----+ +-------+--------+
| | |
| | |
| 1:N | |
v | |
+----------+ | |
| Register | v v
+----+-----+ +----------+ +----------------+
| |Inventory | | Adjustment |
| | Item | | Item |
| 1:N +----------+ +----------------+
v
+----------+
|CashDrawer|
+----------+
+----------+ 1:N +----------+ 1:N +----------+
| Customer |------------->| Sale |------------->| LineItem |
+----+-----+ +----+-----+ +----------+
| |
| | 1:N
| 1:N v
v +----------+
+----------+ | Payment |
| Credit | +----------+
+----------+
+----------+ N:1 +----------+ 1:N +----------+
| Employee |------------->| Role |------------->|Permission|
+----+-----+ +----------+ +----------+
|
| 1:N
v
+----------+
| Shift |
+----------+
Aggregate Boundaries
Each aggregate has a root entity and encapsulates related entities:
Aggregate Definitions
=====================
SALE Aggregate
+------------------------------------------+
| Sale (Root) |
| +-- SaleLineItem[] (owned) |
| +-- Payment[] (owned) |
| +-- Refund[] (reference: sale_id) |
+------------------------------------------+
INVENTORY_ADJUSTMENT Aggregate
+------------------------------------------+
| InventoryAdjustment (Root) |
| +-- InventoryAdjustmentItem[] (owned) |
+------------------------------------------+
INVENTORY_TRANSFER Aggregate
+------------------------------------------+
| InventoryTransfer (Root) |
| +-- InventoryTransferItem[] (owned) |
+------------------------------------------+
CUSTOMER Aggregate
+------------------------------------------+
| Customer (Root) |
| +-- CustomerAddress[] (owned) |
| +-- StoreCredit[] (reference) |
| +-- LoyaltyTransaction[] (reference) |
+------------------------------------------+
PRODUCT Aggregate
+------------------------------------------+
| Product (Root) |
| +-- ProductVariant[] (owned) |
+------------------------------------------+
Summary
The domain model defines 6 bounded contexts with 30+ entities:
| Context | Entities | Purpose |
|---|---|---|
| Catalog | Product, Variant, Category, PricingRule | Product management |
| Sales | Sale, LineItem, Payment, Refund | Transaction processing |
| Inventory | InventoryItem, Adjustment, Transfer | Stock management |
| Customer | Customer, Address, Credit, Loyalty | Customer management |
| Employee | Employee, Role, Permission, Shift | Staff management |
| Location | Location, Register, Drawer, TaxRate | Store configuration |
All entities use UUIDs as primary keys for distributed system compatibility and avoid any legacy system-specific fields.