Decision Log: Item Identifiers
Purpose
Section titled “Purpose”Tracks the design decisions for the Item Identifiers project: how standard identifiers, additional
QR codes, and labels are modeled and stored on Item, and how the fuzzy /lookup and exact
/by-code search endpoints are shaped and indexed. Round 1 was resolved interactively during the
design session on 2026-07-22 and is transcribed here as the canonical record; Round 2 holds the
questions raised for review and resolved during the same session — all decided.
Decision Table
Section titled “Decision Table”| # | Question | Status | Decision | Round |
|---|---|---|---|---|
| DQ-001 | Which standard identifier set? | Decided | Union: UPC, EAN, GTIN, ISBN, ASIN | R1 |
| DQ-002 | Identifier field shape? | Decided | Flat ItemIdentifiers value object, five optional shape-validated fields | R1 |
| DQ-003 | Storage for QRs and labels? | Decided | jsonb arrays on the item row | R1 |
| DQ-004 | /lookup API shape? | Decided | POST .../lookup?lookup=<s> with Query body; GET .../lookup/{page}?lookup=<s> | R1 |
| DQ-005 | /by-code scope and name? | Decided | Renamed from /by-qr-code; matches QRs and standard identifiers | R1 |
| DQ-006 | GTIN normalization mechanism? | Decided | Query-time candidate expansion over indexed equality (no per-row computation) | R1 |
| DQ-007 | /lookup indexing strategy? | Decided | Generated search_text column + single GIN trigram index | R1 |
| DQ-008 | Frontend phase-1 scope and Search-box parity? | Decided | New fields added to BFF SSRM accessors; phase 1 = BFF routes, types, barcode utilities; UI in phase 2 | R1 |
| DQ-009 | Which fields feed search_text? | Decided | Curated surface + left(notes, 256); card_notes_default excluded | R2 |
| DQ-010 | ISBN-10 vs ISBN-13 handling? | Decided | Both shapes accepted; 10↔13 conversion in CodeCandidates | R2 |
| DQ-011 | /lookup ranking and sort interplay? | Decided | Relevance first, caller sort as tiebreak, eid stable | R2 |
| DQ-012 | /by-code method and response shape? | Decided | GET with code query parameter; plain array, capped | R2 |
| DQ-013 | Lookup machinery location (operations vs common-module)? | Decided | Operations-local, under cards.arda.operations.common.lib | R2 |
| DQ-014 | Write-time validation strictness and uniqueness? | Decided | 400 on invalid shape; no uniqueness constraint | R2 |
| DQ-015 | QR/label list constraints (dedupe, limits)? | Decided | Labels ≤128, QRs ≤64, ≤256 chars/entry, dedupe, order preserved | R2 |
| DQ-016 | Performance target and verification method? | Decided | p95 targets + EXPLAIN CI test + repeatable seeding script | R2 |
Round 1: Design-Session Decisions (2026-07-22)
Section titled “Round 1: Design-Session Decisions (2026-07-22)”Resolved interactively during the design session; transcribed for the record. Options are summarized; the trade-off discussion lives in the session and the Design.
DQ-001: Which standard identifier set?
Section titled “DQ-001: Which standard identifier set?”Context: The brief listed UPC, EAN, GTIN, ASIN; the prototype’s Amazon search validates UPC-A, EAN-8/13, ISBN-10 and returns ASIN — ISBN present, GTIN absent.
| Option | Description | Trade-offs |
|---|---|---|
| A | Brief list: UPC, EAN, GTIN, ASIN | No ISBN; diverges from prototype surface |
| B | Prototype-aligned: UPC, EAN, ISBN, ASIN | No GTIN superset field |
| C | Union: UPC, EAN, GTIN, ISBN, ASIN | Most flexible; slightly more schema surface |
Recommendation: Option B — matches what the front end handles today.
Decision: Option C (union) — UPC, EAN, GTIN, ISBN, ASIN, all optional.
Applied to: Design § Structural Design; Goal § Scope A.
DQ-002: Identifier field shape?
Section titled “DQ-002: Identifier field shape?”Context: Determines the API surface, validation model, and indexability of /by-code.
| Option | Description | Trade-offs |
|---|---|---|
| A | Flat ItemIdentifiers value object — five optional fields flattened to columns via the Component.Sub pattern | Idiomatic; each column indexable; adding a sixth namespace later requires a migration |
| B | Namespaced list [{namespace, value}] as jsonb (mirrors prototype enrichment engine) | Extensible without migration; foreign to the module’s flattening idiom; weaker typing; harder exact-match indexing |
Recommendation: Option A.
Decision: Option A, with shape-only (no check-digit) validation unifying the prototype’s divergent validators.
Amendment (2026-07-22 review): each identifier kind is a distinct @JvmInline value class
(Upc, Ean, Gtin, Isbn, Asin) with a smart constructor returning Result — never a
naked String. General rule: domain-meaningful values always get a type alias or value class
(added to the Kotlin coding standards and the front-end equivalent).
Applied to: Design § Class Diagram, § Key Classes.
DQ-003: Storage for Additional QRs and Labels?
Section titled “DQ-003: Storage for Additional QRs and Labels?”Context: The item table is bitemporal; child tables multiply write and join complexity, while jsonb rides the version row.
| Option | Description | Trade-offs |
|---|---|---|
| A | jsonb arrays on the item row (demand-module Exposed precedent) | One write; exact containment via GIN jsonb_path_ops; fuzzy on labels via trigram over labels::text (approximate per-element ranking) |
| B | Bitemporal child tables (item_qr, item_label) | Textbook per-column indexing; substantially more code; temporal parent–child joins with documented alias pitfalls |
| C | Delimited string-encoded columns | Cheapest; fragile exact matching via delimiter tricks |
Recommendation: Option A.
Decision: Option A.
Applied to: Design § Structural Design, § Persistence.
DQ-004: /lookup API shape?
Section titled “DQ-004: /lookup API shape?”Context: The brief proposed string + page number + page size, which conflicts with the module’s
cursor-paged POST {resource}/query convention; lookup-items (typeahead) already exists with
different semantics.
| Option | Description | Trade-offs |
|---|---|---|
| A | GET /lookup?string=&page=&size= | Matches brief; breaks the query/cursor convention; second pagination model to maintain |
| B | Follow /query shape: POST .../lookup?lookup=<s> with Query body (filter, sort, paginate); GET .../lookup/{page}?lookup=<s> for subsequent pages | Convention-consistent; composes filters with fuzzy matching |
Recommendation: Option B.
Decision: Option B.
Applied to: Design § API Contract; Project Plan Phase 2.
DQ-005: /by-code scope and name?
Section titled “DQ-005: /by-code scope and name?”Context: A physical UPC barcode is scannable like a QR-style code; restricting the route to the Additional QRs list would force clients to call two endpoints per scan.
| Option | Description | Trade-offs |
|---|---|---|
| A | /by-qr-code, Additional QRs only | As briefed; scan resolution needs a second call for barcodes |
| B | /by-code, exact match against Additional QRs and any standard identifier | One scan endpoint resolves anything |
Recommendation: Option B.
Decision: Option B — route named /by-code.
Applied to: Design § API Contract, § Behavioral Design.
DQ-006: GTIN normalization mechanism?
Section titled “DQ-006: GTIN normalization mechanism?”Context: A scanner may deliver an EAN-13 rendition of a code stored as 12-digit UPC-A. Matching must bridge zero-padding families without hurting query performance (condition attached to the decision).
| Option | Description | Trade-offs |
|---|---|---|
| A | Store normalized GTIN-14 in a generated column; compare normalized | Extra columns and indexes; write-side complexity |
| B | Store verbatim; expand the queried code into its ≤4 zero-padding equivalents at query time and OR indexed equalities | No schema growth; per-query cost is a handful of b-tree probes |
Recommendation: Option B.
Decision: Option B, conditional on query performance not being compromised — satisfied by construction since expansion happens once per request, not per row.
Applied to: Design § Behavioral Design (/by-code flow), § Key Classes
(CodeCandidates).
DQ-007: /lookup indexing strategy?
Section titled “DQ-007: /lookup indexing strategy?”Context: Initially recommended per-column trigram indexes + OR (matching the existing
lookup-* precedent). The decision to include the full Items-page search text surface (~20 text
columns) flipped the economics: per-column GIN maintenance on a bitemporal table (full row
re-insert per update) and a 20-way BitmapOr per query both degrade.
| Option | Description | Trade-offs |
|---|---|---|
| A | Per-column trigram indexes, OR’d predicates | Idiomatic; acceptable at ~8 columns; poor at ~20 (write amplification, planner overhead) |
| B | Stored generated search_text column concatenating the searchable fields; single GIN gin_trgm_ops index; rank by word distance | One index to maintain and scan; DB-owned consistency; wider row |
Recommendation: Option B (revised from A after the field-set expansion).
Decision: Option B.
Applied to: Design § Persistence, § Behavioral Design (/lookup flow);
Project Plan Phase 1 (migration tasks).
DQ-008: Frontend phase-1 scope and Search-box parity?
Section titled “DQ-008: Frontend phase-1 scope and Search-box parity?”Context: The Items page Search box filters ~27 accessor fields in the BFF (SSRM route over a cached item set); UI enablement is deferred to phase 2.
Decision (recorded without alternatives — directed by the project owner): the new fields join
the BFF SSRM search accessors so Search-box parity is preserved; phase 1 delivers BFF routes for
/lookup and /by-code, TypeScript types and mappers, and shared barcode utilities extended to
ASIN and GTIN. UI surfaces come in phase 2.
Applied to: Design § Implementation Scope (frontend); Project Plan Phase 3.
Round 2: Review Questions (Resolved)
Section titled “Round 2: Review Questions (Resolved)”DQ-009: Which fields feed search_text?
Section titled “DQ-009: Which fields feed search_text?”Context: The generated column defines exactly what /lookup can match. The Items-page Search
box today matches ~27 accessors, including large free-text fields (notes, cardNotesDefault,
up to 8 KiB each) and derived/enum values (costs, dates, print sizes) that are odd targets for
server-side fuzzy text search.
| Option | Description | Trade-offs |
|---|---|---|
| A | Full parity: every text accessor the Search box matches, including notes and card_notes_default | Maximal parity; large search_text values bloat the row and the GIN index; noise matches from long prose |
| B | Curated surface: name, description, internal_sku, gl_code, use_case, classification type/subtype, the four locator fields, supplier/supply names and SKUs (both slots), manufacturer name, the five identifiers, labels | Lean index; omits notes — a Search-box hit on notes would not be a /lookup hit |
| C | Option B + notes + card_notes_default | Full text parity minus non-text accessors; medium index size; % set-similarity degrades on long documents |
| D | Option B + truncated notes via left(notes, N) in the generation expression | Bounds bloat and ranking noise while keeping the leading, identifying portion of notes searchable; left() is immutable so it is legal in a generated column |
Recommendation: Option B originally; revised to Option D during the follow-up given the
ssrm-direct-be-query trajectory (backend lookup should trend toward a superset of the Search
box).
Decision: Option D, modified — left(coalesce(notes, ''), 256) included;
card_notes_default excluded. Labels participate with their complete contents (untruncated; the
DQ-015 per-entry bound is what limits them).
Applied to:
- Design § Persistence and Migrations (generation expression), § Key Classes
- Project Plan Phase 1, T-05
DQ-010: ISBN-10 vs ISBN-13 handling?
Section titled “DQ-010: ISBN-10 vs ISBN-13 handling?”Context: The prototype validates ISBN-10 only. ISBN-13s are EAN-13s (978/979 prefix); ISBN-10 ↔ ISBN-13 conversion requires check-digit recomputation, which DQ-002 excluded from phase 1.
| Option | Description | Trade-offs |
|---|---|---|
| A | Single isbn field accepting both ISBN-10 and ISBN-13 shapes; no cross-form conversion in /by-code expansion | Simple; a scan of the ISBN-13 form will not find an item stored as ISBN-10 (and vice versa) |
| B | Accept both shapes and add 10↔13 conversion (with check-digit computation) to CodeCandidates | Complete matching; ~15-line pure function; the physical-book barcode is always the EAN-13 form, so without conversion the common scan case misses |
Recommendation: Option A originally; revised to Option B during the follow-up — the cost is a
contained pure function, and the miss case (scan 978… vs stored ISBN-10) is the common one for
books. Write-time validation stays shape-only per DQ-002; the check-digit computation lives only
inside the conversion.
Decision: Option B. Conversion is one-directional for 979… ISBN-13s (no ISBN-10
equivalent exists).
Applied to:
- Design § Key Classes (
ItemIdentifiers,CodeCandidates), § Behavioral Design - Project Plan Phase 1, T-02
DQ-011: /lookup ranking and sort interplay?
Section titled “DQ-011: /lookup ranking and sort interplay?”Context: The Query body carries an optional sort; fuzzy search has an intrinsic relevance
order (trigram word distance).
| Option | Description | Trade-offs |
|---|---|---|
| A | Relevance first: order by search_text <->> lookup, then the caller’s sort, then eid as stable tiebreak | Best default UX; caller sort only breaks ties |
| B | Caller’s sort wins when present; relevance only as default | Predictable for grid-style consumers; can bury the best matches |
Recommendation: Option A — /lookup is relevance-oriented by definition; grid-style listing
already has /query.
Decision: Option A.
Applied to:
- Design § Key Classes (
ItemUniverse), § API Contract
DQ-012: /by-code method and response shape?
Section titled “DQ-012: /by-code method and response shape?”Context: Exact-match resolution of one code; expected cardinality 0–few (identifiers are not enforced unique — see DQ-014).
| Option | Description | Trade-offs |
|---|---|---|
| A | GET .../by-code?code=<c> returning a plain JSON array of full item records (no pagination) | Simple, cache-friendly, matches read semantics; unbounded in pathological data |
| B | POST .../by-code returning a PageResult | Convention-heavy for a 0–few result set; consistent envelope |
Recommendation: Option A, with a documented server-side cap (e.g. 50) as a safety bound.
Decision: Option A — the code text travels as the code query parameter; server-side cap
of 50 (configurable).
Applied to:
- Design § API Contract (
/by-code), § Behavioral Design
DQ-013: Lookup machinery location?
Section titled “DQ-013: Lookup machinery location?”Context: The trigram operators (%, <->>) live in common-module; the generated-column
lookup and the jsonb containment operator are new. The project’s declared repos are operations,
arda-frontend-app, documentation.
| Option | Description | Trade-offs |
|---|---|---|
| A | Operations-local: ItemUniverse implements lookupPage/byCode with module-level Exposed expressions (jsonb @> operator defined in operations) | No common-module release in the critical path; second consumer would duplicate |
| B | Generalize into common-module AbstractUniverse now | Reusable; adds a common-module release + version-catalog bump + longer merge chain |
Recommendation: Option A — promote to common-module when a second module needs it.
Decision: Option A, modified — the reusable pieces (jsonb containment operator, any
generic lookup helpers) live under the operations-wide cards.arda.operations.common.lib
package, not inside reference/item, to mark a clean separation and ease later promotion to
common-module.
Applied to:
- Design § Key Classes (
ItemUniverse), § Files to Create
DQ-014: Write-time validation strictness and uniqueness?
Section titled “DQ-014: Write-time validation strictness and uniqueness?”Context: Identifier fields are shape-validated (DQ-002). Two orthogonal choices: what happens
on invalid input, and whether identifiers must be unique per tenant. The brief’s “return a Page of
items” for /by-code implies duplicates are legal.
| Option | Description | Trade-offs |
|---|---|---|
| A | Reject invalid shapes with a 400 (smart-constructor Result per module convention); no uniqueness constraint | Consistent error channel; duplicate identifiers across items allowed — /by-code may return several items |
| B | Accept any string, no validation | Garbage accumulates; /by-code normalization becomes unreliable |
| C | Option A + per-tenant uniqueness on each identifier | Strong integrity; conflicts with legitimate cases (same UPC stocked as two internal items) and complicates bitemporal writes |
Recommendation: Option A.
Decision: Option A.
Applied to:
- Design § Key Classes (
ItemIdentifiers), § Testing Strategy
DQ-015: QR/label list constraints?
Section titled “DQ-015: QR/label list constraints?”Context: Unbounded lists on a bitemporal row multiply storage per version and affect the
search_text width.
| Option | Description | Trade-offs |
|---|---|---|
| A | Server-enforced bounds: ≤64 entries per list, ≤256 chars per entry, exact-duplicate entries rejected, order preserved | Predictable rows and indexes; limits are generous for the use case |
| B | Unbounded, duplicates allowed | No arbitrary limits; pathological rows possible |
Recommendation: Option A (limits configurable in module config, asserted from config in tests). Order preservation rationale (follow-up): jsonb arrays make it free, round-trip fidelity matches user expectations, and it keeps a “first QR is primary” phase-2 semantic open; the cost — a pure reorder writes a new bitemporal version — is defensible behavior, and matching is order-independent either way.
Decision: Option A, modified — labels ≤ 128 entries, additional QRs ≤ 64 entries, ≤ 256 chars per entry, exact duplicates rejected, order preserved; limits configurable, tests assert from config.
Applied to:
- Design § Key Classes (
Item), § Testing Strategy - Project Plan Phase 1, T-03
DQ-016: Performance target and verification method?
Section titled “DQ-016: Performance target and verification method?”Context: “Very performant” needs a number and a way to verify it without flaky wall-clock assertions in CI.
| Option | Description | Trade-offs |
|---|---|---|
| A | Target p95 ≤ 100 ms for /lookup and ≤ 20 ms for /by-code at 50k items/tenant; verify via an EXPLAIN-based integration test asserting index usage (no seq scan) plus a non-CI seeded benchmark script | Deterministic CI; timing evidence gathered manually per release |
| B | Wall-clock assertions in CI against a seeded container | Direct evidence; notoriously flaky in shared CI runners |
Recommendation: Option A.
Decision: Option A, modified — the manual seeding is performed by a checked-in test script so the benchmark is easily repeatable.
Applied to:
- Design § Testing Strategy
- Project Plan Phase 2, T-11
Post-Review Corrections (2026-07-22)
Section titled “Post-Review Corrections (2026-07-22)”Owner review of the committed design surfaced four corrections, applied before implementation started:
- Missing Service layer (structural) — the class diagram omitted
ItemEndpointandItemService. Corrected: the diagram now shows all four layers of the Data Authority pattern. - Typed domain values — identifier fields were drawn as
String?. Corrected per the general rule (never naked base types for domain values): value classesUpc/Ean/Gtin/Isbn/Asin(DQ-002 amendment); the rule itself was added to the Kotlin coding standards page and the React component design page. - Missing Service layer (behavioral,
/lookup) — the sequence went endpoint → universe. Corrected:ItemService.lookupItemscoordinates the read and establishes theinTransaction(db)boundary. - Business logic in the endpoint (
/by-code) — the endpoint calledCodeCandidates.expand. Corrected: expansion, the result cap, and the transaction boundary live inItemService.findByCode; the endpoint validates the request and formats the response only.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved