ADR-0016: Issue Tracking Service Architecture¶
Status¶
PROPOSED
Date: 2026-02-17
Changelog¶
| Date | Revision | Description | Author |
|---|---|---|---|
| 2026-02-17 | 1.0 | Initial version | Loryth Team |
Context¶
The loryth-issues service has a basic skeleton (Issue, Project, Status, Workflow, IssueType) with six SQL migrations. To evolve it into a complete issue tracker, several architectural decisions must be made covering multi-tenant isolation, ordering, workflow automation, board design, issue hierarchy, custom fields, deletion strategy, and search integration.
These decisions affect the data model, API design, and integration points across the platform. They must be made cohesively to ensure a consistent and extensible system.
Decision¶
This ADR captures eight interconnected architectural decisions for the issue tracking service.
Decision 1: Space-Scoped Catalogs via space_id Foreign Key¶
Every catalog entity (Status, Workflow, IssueType, CustomFieldDefinition, LinkTypeDefinition) and Project receives a space_id: UUID column referencing an external Space entity owned by a future core-platform service. This provides organizational isolation — each Space has its own set of statuses, workflows, and types that are shared across projects within that Space but invisible to other Spaces.
The Space entity is not owned by loryth-issues. The service stores only the UUID reference and trusts that the referenced Space exists. No foreign key constraint to an external service's table; referential integrity is maintained at the application level.
Decision 2: LexoRank for Ordering¶
Issues use lexicographic string ranking for manual drag-and-drop reordering. The rank column (VARCHAR(255)) stores a string that determines sort order. New ranks are generated as midpoints between adjacent items using a 36-character alphabet (0-9a-z).
Rank is a global property of the issue — the same rank applies everywhere (backlog, board, sprint view). There is no per-context ranking.
When a rank string exceeds 50 characters (indicating excessive splitting), a background rebalancing job reassigns evenly-spaced ranks to all issues in the project under an advisory lock.
Decision 3: Auto-Walk Transitions¶
Board columns may represent statuses that are not directly connected by a workflow transition. When an issue is dragged to a column whose primary status has no direct transition from the current status, the system performs a BFS (breadth- first search) through the workflow transition graph to find the shortest valid path.
If a valid path exists, the issue's status is set to the target status directly. Only one activity log entry is recorded (status_changed from the original to the final status). Intermediate statuses are never visible to users and never recorded.
If no valid path exists, the drag operation is rejected with a 422 Unprocessable Entity response.
Decision 4: Board Column Model with Primary Drop Status¶
Each board column can display issues in multiple statuses (e.g., an "In Review" column showing both "Code Review" and "QA Review" issues). To resolve ambiguity when dragging an issue into a column, each column designates a primary_status_id — the status that is set when an issue is dropped into that column.
Constraints: - The primary status must be one of the statuses mapped to that column - Each status appears in at most one column per board - Statuses not mapped to any column do not appear on the board (they exist only in backlog/list views)
Decision 5: Hierarchy via Type Constraints¶
Issues can have parent-child relationships through the existing parent_id self-reference. Rather than enforcing a fixed hierarchy (e.g., Epic > Story > Task), allowed parent-child type combinations are configured per project via a type_hierarchy_rule table.
Default rules: Epic can parent {Story, Task, Bug}; Story can parent {Task, Bug}; Task and Bug cannot parent other issues. Projects can customize these rules.
There is no dedicated "sub-task" issue type. Any type can be a child if the hierarchy rules allow it. Cycle prevention is enforced by walking the ancestor chain on parent assignment.
Decision 6: Entity-Attribute-Value for Custom Fields¶
Custom fields use an EAV model with two tables:
custom_field_definition: Space-scoped, defines field name, type (text, number, date, single_select, multi_select, user), options (JSONB for select types), and whether the field is requiredcustom_field_value: Links an issue to a field definition with typed value columns (value_text,value_number,value_date,value_json)
Only one value_* column is populated based on the field type. Field definitions are enabled per project via project_custom_field junction table.
Decision 7: Soft Delete with deleted_at Timestamp¶
Issues support soft deletion via a deleted_at: TIMESTAMP column. Soft-deleted issues are excluded from all queries by default (repository-level WHERE deleted_at IS NULL filter). A dedicated restore endpoint clears deleted_at.
Cascade behavior: soft-deleting an issue also soft-deletes all child issues. Comments and attachments remain intact (they are accessed through the issue, which is hidden).
Issue keys (PROJ-123) are never reused — soft-deleted issues retain their key in perpetuity.
Hard deletion via a background job with configurable retention (default 30 days) is planned for a future iteration.
Decision 8: Search Delegation to ai-service¶
Full-text search is delegated to the ai-service rather than implemented locally. The issue tracking service publishes indexing events (IssueIndexEvent, CommentIndexEvent, IssueDeleteIndexEvent) to ai-service via RabbitMQ. Search queries are proxied through ai-service's API.
When ai-service is unavailable, the system falls back to basic SQL ILIKE filtering on issue title and description.
Consequences¶
Positive¶
- Space-scoped catalogs provide clean multi-tenant isolation without coupling to a Space service
- LexoRank enables O(1) insert/move operations for drag-and-drop with no renumbering
- Auto-walk transitions create a seamless board drag UX while still enforcing workflow rules
- Primary drop status eliminates ambiguity in multi-status board columns
- Type hierarchy constraints are flexible and configurable per project
- EAV custom fields are queryable with standard SQL joins and indexable per field
- Soft delete provides a safety net against accidental data loss
- Search delegation keeps the issues service focused and leverages ai-service's semantic capabilities
Negative¶
- Space UUID referential integrity relies on application-level enforcement (no FK constraint)
- LexoRank requires periodic rebalancing when rank strings grow too long
- Auto-walk BFS adds computational overhead (mitigated: workflows are typically small graphs)
- EAV queries are more complex than JSONB column access for custom fields
- Soft delete requires
WHERE deleted_at IS NULLon all queries (enforced at repository level) - Search depends on ai-service availability (mitigated: SQL ILIKE fallback)
Neutral¶
- All decisions are additive to the existing schema — no existing tables are dropped
- Board and sprint features are optional per project (not all projects need boards)
- Custom fields are opt-in per project (enabled through project configuration)
Alternatives Considered¶
-
Space entity owned by loryth-issues
- Description: Create and manage the Space entity within the issues service
- Pros: No external dependency, full control
- Cons: Couples organizational structure to issues; other services also need Spaces
- Why not chosen: Space is a platform-level concept that belongs in a core service
-
Integer position for ordering
- Description: Use an integer
positioncolumn, renumber all items on insert/move - Pros: Simple to understand, deterministic
- Cons: O(n) updates on every reorder operation, poor for concurrent access
- Why not chosen: Unacceptable performance for large backlogs with frequent reordering
- Description: Use an integer
-
Direct transitions only (no auto-walk)
- Description: Require a direct workflow transition for every status change
- Pros: Simpler implementation, explicit control
- Cons: Forces users to move issues through every intermediate status manually
- Why not chosen: Poor UX on boards where columns may skip intermediate statuses
-
JSONB column for custom fields
- Description: Store all custom field values in a JSONB column on the issue table
- Pros: Simple schema, flexible, fast reads
- Cons: Not indexable per field, no schema validation, difficult to query across issues
- Why not chosen: Need for per-field indexing and structured queries outweighs simplicity
-
PostgreSQL tsvector for search
- Description: Use PostgreSQL's built-in full-text search with GIN indexes
- Pros: No external dependency, transactionally consistent
- Cons: Limited relevance ranking, no semantic search, additional index maintenance
- Why not chosen: ai-service provides superior search with semantic understanding
-
Hard delete only
- Description: Permanently delete issues on deletion
- Pros: Simpler implementation, no query filters needed
- Cons: No recovery from accidental deletion, data loss risk
- Why not chosen: Issue data is valuable and accidental deletion must be recoverable
References¶
- Issues Tracking Service Roadmap
- TDD-0004 — Issue Tracking Domain Model
- TDD-0005 — Issue Tracking Workflows and Boards
- TDD-0006 — Issue Tracking Collaboration
- TDD-0007 — Issue Tracking Audit, Search and Reporting
- TDD-0002 — File Storage System
- TDD-0003 — Notification Service
- ADR-0003 — Event-Driven Architecture
- ADR-0004 — Service Boundary Definition