Architecture Decision Records (ADRs) — Laundry Management System¶
Document Information¶
| Field | Value |
|---|---|
| Project | Laundry Management System |
| Version | 1.0 |
| Document Type | Architecture Decision Records |
ADR-001: Custom Full-Stack Application over ERPNext¶
Date: 2026-05-10
Status: Accepted
Context¶
We evaluated ERPNext (MariaDB) as a rapid-development platform. ERPNext provides free accounting, user management, and inventory modules. The frontend would be a custom Angular application consuming ERPNext REST APIs ("Headless ERPNext").
Decision¶
We chose a custom full-stack application (ASP.NET Core + Angular + PostgreSQL) over ERPNext.
Rationale¶
-
Offline branch sync — ERPNext is designed for centralized online use. Merging two offline ERPNext databases (with complex internal references and auto-incrementing IDs) is extremely risky. A custom app with UUID primary keys makes offline merge safe and deterministic.
-
Unique laundry workflows — Carpet receipts without price/area, piece-level garment barcode tracking, and tailoring embedded in laundry invoices are fundamentally non-standard flows that require heavy customization in ERPNext.
-
Simpler total stack — Building a focused laundry application with a custom double-entry ledger is simpler than fighting ERPNext's assumptions about sales, inventory, and payment flows.
-
PostgreSQL optionality — ERPNext requires MariaDB. A custom app supports both PostgreSQL and SQLite (for offline branches).
Consequences¶
- Accounting must be built from scratch (see ADR-003).
- User management, roles, and permissions must be built from scratch.
- We retain full control over every aspect of the application.
ADR-002: Tauri over Electron for Desktop Client¶
Date: 2026-05-10
Status: Accepted
Context¶
Offline branches need a desktop application. Users must not use a browser to access the system. We evaluated Electron (bundled Chromium) and Tauri (native OS WebView).
Decision¶
We chose Tauri over Electron.
Rationale¶
-
Binary size: Tauri ~5-10 MB + Angular build. Electron ~200 MB + Angular build. For USB-delivered updates to offline branches, size matters.
-
Performance: Tauri uses the OS-native WebView (WebView2 on Windows, WebKit on macOS, WebKitGTK on Linux). Electron bundles its own Chromium, consuming significantly more RAM.
-
Cross-platform: Tauri builds native installers for Windows (.msi), macOS (.dmg), and Linux (.deb) from the same codebase.
-
Plugin ecosystem: Tauri v2 plugins cover all our needs: shell (Docker commands), updater (auto-update), dialog (file picker), fs (config/license files), window-state, store, process, os.
-
Rust is minimal: The Tauri Rust backend is ~80 lines. No deep Rust expertise required.
Consequences¶
- CI/CD must build Tauri on 3 platforms (Windows, macOS, Linux) — done via GitHub Actions matrix.
- WebView2 runtime may need a redistributable on older Windows 10 machines.
- Auto-update for Type 1 (online) is built-in via
@tauri-apps/plugin-updater.
ADR-003: Accrual Double-Entry Accounting over Cash Basis¶
Date: 2026-05-10
Status: Accepted
Decision¶
We chose accrual-basis double-entry accounting over cash-basis or single-entry.
Rationale¶
-
Deferred payments — The system supports "pay later" for registered customers (hotels, companies). Accrual accounting correctly records revenue when service is delivered, not when cash arrives.
-
Customer credit tracking — Carpet advance surpluses, refund credits, and compensation payments create customer balances. Accrual tracks these as liabilities until applied.
-
Auditability — Double-entry (every debit has a matching credit) creates a built-in integrity check:
SUM(debit) == SUM(credit)must always hold. This is automatable in tests. -
Professional reporting — Accrual supports proper Income Statement (P&L) and Balance Sheet. Cash basis does not.
Consequences¶
- A General Ledger table and Journal Entry system must be built.
- 13 accounting flows must be implemented and tested with automated SUM validation.
- The Accounting Rules Engine adds complexity but enables configurable settlement models for carpet companies and tailors.
ADR-004: UUID Primary Keys over Auto-Incrementing IDs¶
Date: 2026-05-10
Status: Accepted
Decision¶
All primary keys are UUIDs (C# Guid) generated in the application layer, never by the database.
Rationale¶
-
Offline branch merge — When an offline branch exports data and the central server imports it, auto-incrementing IDs would collide. UUIDs are globally unique, enabling safe UPSERT merges.
-
No central ID generator — Offline branches have no internet. UUIDs can be generated independently on every branch.
-
Re-import safety — Importing the same sync file twice is idempotent with UUIDs (
INSERT ... ON CONFLICT (id) DO UPDATE).
Consequences¶
- UUIDs are 16 bytes vs 4/8 bytes for integers. Storage and index size are larger but negligible at this scale.
- UUIDs are slightly slower for B-tree index operations, but the difference is not meaningful for a laundry POS with <1M records per table.
- EF Core is configured with
ValueGeneratedNever()for all UUID keys.
ADR-005: Hybrid Reporting (Backend Calculates, Frontend Renders)¶
Date: 2026-05-10
Status: Accepted
Decision¶
All report calculations happen in the backend (SQL aggregates, running balances, financial totals). The backend returns JSON. The frontend handles all visual rendering, printing, and Excel/PDF export.
Rationale¶
-
Customization pain — Backend PDF/HTML report libraries (QuestPDF, RDLC) require code changes and redeployment for every template tweak. Frontend templates can be updated independently.
-
Accounting accuracy — Financial calculations must never run in JavaScript.
0.1 + 0.2 !== 0.3in JS. PostgreSQLnumeric(18,4)and C#decimalare exact. -
Single source of truth — Backend computes values once. The frontend renders the same JSON for display, print, Excel, or PDF — no recalculation means no inconsistencies.
-
Offline branch simplicity — Offline branches already run a local PostgreSQL. Reports are identical with zero extra infrastructure.
Consequences¶
- Backend exposes "Report Data APIs" (not "Rendered Report APIs"). Frontend services handle export.
- PrimeNG tables, SheetJS, html2canvas, jspdf on the frontend.
- Running balances and complex aggregations use Dapper + raw SQL for performance.
ADR-006: Traefik over Nginx¶
Date: 2026-05-10
Status: Accepted
Decision¶
We chose Traefik as the reverse proxy for all deployments.
Rationale¶
-
Docker-native — Traefik auto-discovers containers via Docker labels. Nginx requires manual config reloads.
-
Auto-SSL — Traefik auto-provisions and renews Let's Encrypt certificates. Nginx requires certbot with cron-based renewal.
-
Static certificates — For offline branches, Traefik reads pre-generated mkcert
.pemfiles. One.envvariable (CERT_MODE=static|acme) switches between modes. -
Dashboard — Traefik has a built-in dashboard at
:8080. Nginx requires third-party tools. -
Zero-downtime reloads — Traefik detects container changes and updates routing without restarting.
Consequences¶
- All deployment types use the same Traefik container and configuration.
- mkcert must be run once on each offline branch PC during setup.
ADR-007: Seq over ELK/Grafana for Logging¶
Date: 2026-05-10
Status: Accepted
Decision¶
We chose Seq for centralized logging on every deployment.
Rationale¶
-
Single container — Seq is one Docker container. ELK (Elasticsearch + Logstash + Kibana) or Loki + Grafana require 3+ containers.
-
Works offline — Seq runs locally on the branch PC. ELK/Grafana are designed for server-based deployments.
-
Beautiful UI — Seq has a searchable, filterable log viewer that non-technical support staff can use.
-
Serilog integration — One NuGet sink (
Serilog.Sinks.Seq). Config is one line. -
OpenTelemetry exporter — Seq v2024+ accepts OTLP traces. We get distributed tracing with zero extra infrastructure.
Consequences¶
- On offline branches, Seq is accessed at
http://localhost:5341. On online servers, it's athttps://logs.domain.com. - Seq is free for local use. The free tier is unlimited for our scale.
ADR-008: Angular + NgRx over Plain RxJS or React¶
Date: 2026-05-10
Status: Accepted
Decision¶
We use Angular with NgRx for state management.
Rationale¶
-
Angular chosen early — The client specified Angular early in the project. PrimeNG was confirmed by the client.
-
NgRx for complex state — A laundry POS has complex interdependent state: invoice items + customer classification → pricing, payments → remaining balances, shift → expected cash. NgRx's Redux pattern makes this predictable and debuggable.
-
Redux DevTools — Every state change is visible in the browser's Redux DevTools extension. Critical for debugging accounting-related UI bugs.
-
Facade pattern — Components never import Store directly. The
InvoiceFacadeabstracts NgRx, keeping components clean and testable. If we ever migrate away from NgRx, only facades change. -
NgRx Entity — Built-in
@ngrx/entityprovides efficient CRUD operations on collections with adapter pattern.
Consequences¶
- NgRx adds boilerplate (actions, reducers, effects, selectors). The
@ngrx/schematicsCLI generators mitigate this. - Learning curve for new developers. The Facade pattern reduces the surface area.
ADR-009: Docker Compose Everywhere, No Kubernetes¶
Date: 2026-05-10
Status: Accepted
Decision¶
All deployments use Docker Compose as the orchestration layer. Kubernetes is not used.
Rationale¶
-
Single-host deployments — Every customer runs on one server or one branch PC. Docker Compose is the standard for single-host deployments. Kubernetes adds massive complexity for zero benefit.
-
One docker-compose.yml — All three deployment types (online, offline, offline-sync) use the same file, toggled by
.envvariables. -
Offline branch compatibility — Docker Compose works fully offline. Kubernetes assumes a cluster with an API server, which is impractical for air-gapped branch PCs.
-
Simpler operations —
docker compose up -danddocker compose pullare commands the customer's IT person can learn in minutes. Kubernetes requires specialized training.
Consequences¶
- No auto-scaling or rolling updates with health checks. Not needed — this is a single-instance application.
- Manual rollback:
VERSION=prev docker compose up -d.
ADR-010: ASP.NET Core + .NET 10 over Node.js, Django, or Go¶
Date: 2026-05-10
Status: Accepted
Decision¶
We chose ASP.NET Core on .NET 10 for the backend.
Rationale¶
-
Client preference — The client chose this stack early in the project.
-
C# decimal type —
decimalis a 128-bit type with exact decimal representation. Essential for accounting. JavaScript and Python have no native exact decimal type (Python'sDecimalis a library, not a primitive). -
Strong typing — C# nullable reference types prevent null-reference errors at compile time. Accounting code benefits from compile-time safety.
-
Performance — ASP.NET Core is among the fastest web frameworks in independent benchmarks (TechEmpower).
-
EF Core + Dapper — We can seamlessly mix an ORM (master data, CRUD) with raw SQL (reports, ledger queries) in the same project.
Consequences¶
- The team must be comfortable with C# and .NET.
- Docker images are larger than Go or Node.js equivalents, but the
alpine-based runtime keeps them reasonable (~120 MB).
Revision History¶
| Date | Version | Author | Changes |
|---|---|---|---|
| 2026-05-10 | 1.0 | System Architect | Initial 10 ADRs |