Skip to content

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

  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. Binary size: Tauri ~5-10 MB + Angular build. Electron ~200 MB + Angular build. For USB-delivered updates to offline branches, size matters.

  2. 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.

  3. Cross-platform: Tauri builds native installers for Windows (.msi), macOS (.dmg), and Linux (.deb) from the same codebase.

  4. 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.

  5. 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

  1. 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.

  2. Customer credit tracking — Carpet advance surpluses, refund credits, and compensation payments create customer balances. Accrual tracks these as liabilities until applied.

  3. 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.

  4. 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

  1. 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.

  2. No central ID generator — Offline branches have no internet. UUIDs can be generated independently on every branch.

  3. 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

  1. Customization pain — Backend PDF/HTML report libraries (QuestPDF, RDLC) require code changes and redeployment for every template tweak. Frontend templates can be updated independently.

  2. Accounting accuracy — Financial calculations must never run in JavaScript. 0.1 + 0.2 !== 0.3 in JS. PostgreSQL numeric(18,4) and C# decimal are exact.

  3. 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.

  4. 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

  1. Docker-native — Traefik auto-discovers containers via Docker labels. Nginx requires manual config reloads.

  2. Auto-SSL — Traefik auto-provisions and renews Let's Encrypt certificates. Nginx requires certbot with cron-based renewal.

  3. Static certificates — For offline branches, Traefik reads pre-generated mkcert .pem files. One .env variable (CERT_MODE=static|acme) switches between modes.

  4. Dashboard — Traefik has a built-in dashboard at :8080. Nginx requires third-party tools.

  5. 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

  1. Single container — Seq is one Docker container. ELK (Elasticsearch + Logstash + Kibana) or Loki + Grafana require 3+ containers.

  2. Works offline — Seq runs locally on the branch PC. ELK/Grafana are designed for server-based deployments.

  3. Beautiful UI — Seq has a searchable, filterable log viewer that non-technical support staff can use.

  4. Serilog integration — One NuGet sink (Serilog.Sinks.Seq). Config is one line.

  5. 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 at https://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

  1. Angular chosen early — The client specified Angular early in the project. PrimeNG was confirmed by the client.

  2. 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.

  3. Redux DevTools — Every state change is visible in the browser's Redux DevTools extension. Critical for debugging accounting-related UI bugs.

  4. Facade pattern — Components never import Store directly. The InvoiceFacade abstracts NgRx, keeping components clean and testable. If we ever migrate away from NgRx, only facades change.

  5. NgRx Entity — Built-in @ngrx/entity provides efficient CRUD operations on collections with adapter pattern.

Consequences

  • NgRx adds boilerplate (actions, reducers, effects, selectors). The @ngrx/schematics CLI 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

  1. 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.

  2. One docker-compose.yml — All three deployment types (online, offline, offline-sync) use the same file, toggled by .env variables.

  3. Offline branch compatibility — Docker Compose works fully offline. Kubernetes assumes a cluster with an API server, which is impractical for air-gapped branch PCs.

  4. Simpler operationsdocker compose up -d and docker compose pull are 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

  1. Client preference — The client chose this stack early in the project.

  2. C# decimal typedecimal is a 128-bit type with exact decimal representation. Essential for accounting. JavaScript and Python have no native exact decimal type (Python's Decimal is a library, not a primitive).

  3. Strong typing — C# nullable reference types prevent null-reference errors at compile time. Accounting code benefits from compile-time safety.

  4. Performance — ASP.NET Core is among the fastest web frameworks in independent benchmarks (TechEmpower).

  5. 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