arc42 §8 — Cross-Cutting Concepts
Status: Draft v0.1 · Date: 2026-07-20 Purpose: Patterns that apply across modules. Decided once here rather than inconsistently in twelve places.
8.1 Bilingual Content (Arabic / English)#
OQ-12 · QAS-LOC-01, QAS-PERF-02 · CON-12
Qwizin is a Saudi platform with an Arabic BRD. Arabic is not a localisation afterthought — it is the primary language of the market.
Storage: split by content type, not by field length#
| Content | Mechanism | Why |
|---|---|---|
| Short attributes — listing names, category labels, tags, short descriptions | JSON column ({"ar": "...", "en": "..."}) |
One row, no joins, simple writes. The majority of translatable content. |
| Long-form — training modules, templates, assessment questions | Separate translation table | These need independent lifecycles: Arabic may be published while English is in review, each with its own author, review state, and version history |
The distinction is not field length — it is whether the translation has an independent lifecycle. A JSON blob cannot express "the Arabic version is approved and the English one isn't", and retrofitting that later means migrating your most valuable content.
Arabic normalisation — mandatory, not optional#
Users type أ / إ / آ / ا interchangeably, ى and ي interchangeably, and almost never type diacritics. Without folding, search recall collapses silently and totally — a user searches in Arabic, gets nothing, and concludes the directory is empty.
Normalisation pipeline (applied identically at index time and to queries):
strip tatweel (ـ) → strip diacritics/harakat → fold أإآٱ → ا
→ fold ة → ه → fold ى → ي → normalise Arabic-Indic digits ٠-٩ → 0-9
Own this in the application layer. It is roughly 150 lines of well-tested string handling, it delivers the higher-impact half of Arabic search quality on any engine, and it makes the engine choice reversible rather than a one-way door (
R-11). This is the single most portable investment available.
Additional Arabic concerns#
| Concern | Handling |
|---|---|
| Broken plurals (كتاب → كتب, بيت → بيوت) | No light stemmer handles these — they are common for product and category nouns. Plan a synonym/lemma dictionary regardless of engine; it is the highest-leverage relevance investment. |
| Collation | Default collation gives codepoint order, not Arabic alphabetical order. Specify an Arabic collation for any user-visible sorted list. |
| Directional control characters (RLM/LRM) | Strip on input — they break exact matching invisibly |
| Mixed LTR/RTL ("iPhone 15 برو") | Grapheme-aware truncation for previews; never split a cluster or strand a direction mark |
| Slugs | Arabic slugs need transliteration or percent-encoding — decide explicitly |
API response shape#
Resolve server-side by default; return the resolved string plus metadata.
{ "id": 42, "name": "مطعم الرياض", "locale": "ar", "locale_fallback": false }
| Rule | Rationale |
|---|---|
Use an explicit ?locale= parameter as the cache key, not Accept-Language |
Accept-Language is high-cardinality in the wild (ar, ar-SA, ar-SA,ar;q=0.9,en;q=0.8…) and shreds CDN hit rates. Two supported values = two cache entries. |
Accept-Language normalised to the supported set, used only as a default |
— |
Return all locales on detail and admin endpoints (?locales=all) |
Editors are literally editing translations; detail views benefit from instant switching |
Always return locale_fallback |
Silent fallback makes missing translations invisible to the content team. Content gaps are a product problem, not a rendering one. |
Return a direction hint on locale metadata |
So each client doesn't hardcode a locale→direction map |
8.2 Error Handling & API Contract#
Error response shape#
Consistent across every endpoint. Mobile clients must be able to act on errors programmatically.
{
"error": {
"code": "account.not_approved",
"message": "Your account is under review.",
"message_ar": "حسابك قيد المراجعة.",
"details": { "state": "under_review", "submitted_at": "..." },
"correlation_id": "01J..."
}
}
| Element | Purpose |
|---|---|
Machine-readable code |
Clients branch on this, never on message text |
| Localised message | Displayable directly |
details |
Structured context — e.g. the account state, so the app renders the right onboarding screen |
correlation_id |
The user can quote it to support, and it links directly to logs (QAS-OPS-01) |
Status code discipline#
| Code | Use |
|---|---|
400 |
Malformed request |
401 |
Missing/invalid authentication |
403 |
Authenticated, not permitted — and the resource's existence is not a secret |
404 |
Not found or cross-tenant probe — a 403 confirms existence and enables enumeration |
409 |
State conflict (e.g. booking a taken slot) |
422 |
Validation failure with field-level detail |
429 |
Rate limited, with Retry-After |
Failure philosophy#
| Principle | Meaning |
|---|---|
| Fail closed on authorization and tenancy | Ambiguity denies |
| Fail open on non-critical enrichment | Search unavailable → degraded DB listing, not an error page (QAS-AVL-04) |
| Never lose committed work | An assessment result survives a PDF failure; a booking survives a notification failure |
| Errors are events | Authorization denials are the attack signal — log them (§8.5) |
8.3 Idempotency & Concurrency#
Idempotency#
| Surface | Mechanism |
|---|---|
| Client-initiated payment mutations | Client-supplied idempotency key, stored with the result |
| All inbound webhooks | Deduplicate on the provider's event ID — providers will retry |
| Queued jobs | Handlers idempotent; SIGKILL after grace expiry will happen |
| Assessment submission | Second submission for a finalised attempt is rejected, not re-graded |
Duplicate processing of a payment capture is a double charge. This is not a robustness nicety.
Concurrency#
| Contention point | Mechanism |
|---|---|
| Booking slot | Pessimistic lock for the reservation window — double-booking is user-visible and unrecoverable |
| Subscription renewal | Distributed lock per subscription |
ZATCA invoice sequence (if OQ-11 applies) |
Single serialisation point — hash chaining is strictly ordered and cannot be produced concurrently by multiple pods (R-02) |
| Certificate issuance | Transactionally bound to a graded attempt; unique constraint prevents duplicates |
| Counters / aggregates | Atomic increments, never read-modify-write |
8.4 Time, Money & Identifiers#
Time#
| Rule | Rationale |
|---|---|
| Store UTC, always | — |
| Present in Asia/Riyadh by default | KSA-first market |
| Business-hour logic uses Sun–Thu | The Saudi working week — a Mon–Fri assumption is wrong here |
| Never trust a client clock | Assessment time limits enforced against the server-recorded start |
| Booking slots carry an explicit timezone | Required before GCC expansion (CON-12) |
| Hijri dates where user-facing | Consider for certificates and official documents |
Money#
| Rule | Rationale |
|---|---|
| Integer minor units (halalas) — never floats | Floating-point money is a correctness bug waiting for a rounding case |
| Currency is explicit on every amount | CON-12 — GCC expansion means SAR is not forever the only currency |
A Money value object in the shared kernel |
Prevents mixing currencies by accident |
| Store the as-charged amount, not just a reference to a plan | Plan prices change; historical records must not |
| VAT handled explicitly | Not folded into an opaque total (OQ-11) |
Identifiers#
| Type | Use |
|---|---|
| ULID/UUID for anything externally referenced | Listings, bookings, documents, certificates — removes trivial enumeration |
| Sequential integers internally where useful | — |
| Never sequential for certificates | Sequential IDs let anyone enumerate the entire certificate corpus |
Typed ID value objects (UserId, CompanyId) |
Prevents passing a company ID where a user ID belongs — a real source of cross-tenant bugs |
8.5 Observability#
QAS-OPS-01 — a single on-call engineer must diagnose a 02:00 incident without tribal knowledge.
flowchart LR
REQ["Request"] -->|"generate<br/>correlation_id"| API["API"]
API -->|"propagate"| JOB["Queue job"]
API -->|"propagate"| SIDE["Sidecar"]
API -->|"propagate"| EXT["External call"]
API & JOB & SIDE --> LOGS["Structured logs<br/><i>correlation_id on every line</i>"]
API & JOB & SIDE --> TRACE["Traces"]
API & JOB & SIDE --> METRIC["Metrics"]
LOGS & TRACE & METRIC --> ALERT["Alerts on<br/><b>user-facing SLOs</b>"]
| Signal | Approach |
|---|---|
| Correlation ID | Generated at ingress, propagated through every async hop, present on every log line and returned in error responses |
| Traces | Sampled; payment and video flows at 100 % |
| Metrics | RED for API; queue depth + oldest-job age for workers; session count for media |
| Logs | Structured JSON to stdout, shipped off-cluster, PII redacted at the logging layer |
| Alerts | On user-facing SLOs — not CPU graphs |
Business metrics are first-class, not an afterthought: the four SRC-MOM §13 KPIs require event capture designed in from the start. Reconstructing "templates downloaded" from access logs a year later is expensive and inaccurate.
8.6 Configuration & Feature Management#
Configuration hierarchy#
| Layer | Contains | Changes require |
|---|---|---|
| Build-time | Framework config, cached | Deploy |
| Environment | Endpoints, credentials (from secret store) | Restart |
| Runtime / database | Business configuration | Nothing — admin UI |
What must be database configuration, not code#
CON-07 and CON-08 are explicit constraints, and both are trivially satisfied if designed correctly from the start:
- Subscription plans and pricing (
CON-07) - Assessment passing scores (
CON-08) - Featured listing packages
- Required document sets per account type (pending
OQ-02) - Notification templates
- Consultant response SLA, booking expiry windows (pending
OQ-05)
Changing a passing score must never require a release. Anything a business stakeholder will plausibly want to adjust belongs in the admin panel — hardcoding it converts a five-minute business decision into a deployment.
Feature flags#
For CONF-01-dependent training features, and for the gradual rollout of high-risk changes (RLS enforcement, search engine switch). Keep the flag set small and remove flags once decided — a permanent flag is untested branching.
8.7 Testing Strategy#
Ordered by defect-prevention value for this system, not by convention.
| Layer | Focus | Non-negotiable |
|---|---|---|
| Adversarial cross-tenant suite | Every endpoint, both directions | ⭐ QAS-SEC-01 — the control that survives turnover |
| Architecture tests | Module boundaries, User isolation, tenant traits, no env() outside config |
⭐ R-08 |
| Contract tests | No correctness fields in assessment delivery responses | ⭐ QAS-INT-01 |
| Golden-image tests | Arabic PDF rendering over a fixture set of names | ⭐ QAS-LOC-01 |
| Integration | State machines: booking, approval, subscription | High value |
| Unit | Domain logic, grading, money arithmetic | Standard |
| Load | Peak concurrency, burst certificate generation | Before launch |
The four starred layers are unusual and specific to this system's risks. A conventional test pyramid would miss all four, and each guards a failure mode that is silent in production.
8.8 Data Retention & Deletion#
QAS-CMP-01 · ⚠️ Retention periods require legal confirmation (R-03)
| Data | Retention driver | Conflict |
|---|---|---|
| Financial records, invoices | Statutory (likely 7+ years) | Overrides erasure requests |
| Audit ledger | Compliance evidence | ⚠️ Direct tension with PDPL minimisation — must be documented and defended, not improvised |
| Issued certificates | Third-party verification value | Revoke rather than delete? → OQ-07 |
| Assessment attempts | Certificate provenance | Anonymise rather than delete |
Recorded consultations (if OQ-08) |
Dispute window only | Aggressive deletion is the correct default |
| Session logs, telemetry | Operational | Short, automated |
| Marketing/analytics events | Consent-dependent | — |
The enumeration problem#
The hard part of a data-subject request is not deletion — it is finding everything. Once personal data has spread into a search index, a reporting projection, a log pipeline, and backups, "locate all data about this person" is unanswerable without a personal-data inventory maintained as a first-class artifact.
Build the inventory while the system is small. Retrofitting it means auditing every store after the fact.
8.9 Concept Summary#
| Concept | Decided | Where |
|---|---|---|
| Bilingual storage | Split by lifecycle, not length | §8.1 |
| Arabic normalisation | Application-owned pipeline | §8.1 |
| Error contract | Machine-readable code + correlation ID | §8.2 |
| Cross-tenant probes | 404, never 403 | §8.2 |
| Idempotency | Client keys + webhook dedup | §8.3 |
| Booking concurrency | Pessimistic lock | §8.3 |
| Money | Integer minor units + explicit currency | §8.4 |
| Time | UTC stored, Riyadh presented, Sun–Thu week | §8.4 |
| External IDs | ULID — never sequential for certificates | §8.4 |
| Observability | Correlation ID through every async hop | §8.5 |
| Business config | Database, not code (CON-07, CON-08) |
§8.6 |
| Testing | Four system-specific layers above the pyramid | §8.7 |
| Retention | Inventory-first; audit/erasure conflict documented | §8.8 |