Harish Darko

Case study · live system · July 2026

CaseLens

I built CaseLens to help support teams understand why a ticket received its priority, which evidence supports the result, and how to recover failed processing.

114backend tests12frontend tests2browser journeysliveon AWS, ca-central-1

synthetic demo ticket · WEBPayment accepted butcharging did not start“CHARGING_SESSIONPAYMENT_CAPTURE_NO_SESSIONHIGHurgencythe exact wordsthe model quotedset by app rules,not by the modela policy thatreally existsa person can change it
One synthetic ticket, as a reviewer sees it in CaseLens.

The problem

Operators need evidence, processing history, and a safe recovery path.

A priority label is not enough when an operator must decide what to do next or explain what happened later.

Trust
The recommendation needs quoted evidence, policy, and a reason that a reviewer can inspect.
Continuity
Queue and provider failures need durable processing history, bounded retry behavior, and a safe recovery path.
Control
A reviewer must be able to correct the result without erasing the original decision or its trace.

Runtime path

PostgreSQL remains the source of truth.

Pick what happens to the ticket. Each station says what it does, and the ones that sit out fade.

  1. 01React reviewerisolated synthetic workspace

    A reviewer opens a synthetic ticket in their own workspace.

    The reviewer changes the urgency, say from MEDIUM to HIGH.

  2. 02Spring Boot APIvalidation and domain rules

    Saves the ticket, its job and an outbox event in one transaction.

  3. 03Outbox + SQSdurable asynchronous delivery

    The relay sends only IDs to SQS. The ticket text stays in PostgreSQL.

    SQS delivers the same message twice. Queues can do that.

  4. 04Triage workerprovider validation or fallback

    Reloads the ticket, sends redacted text to the model, and checks every quote and policy ID. App rules set the priority.

    The first worker claims the job. The second claim fails its condition, so the job runs once.

    The model quotes words that are not in the ticket. Validation rejects it, and deterministic rules triage the ticket.

    The demo's synthetic provider timeout. The job is recorded as RETRYABLE_FAILURE.

  5. 05Review + recoverytrace, feedback, replay

    The reviewer sees the quoted evidence, the policy and the reason.

    The reviewer still gets a triage result, from the rules instead of the model.

    A reviewer inspects the failure record and replays the job.

    The original result and its trace stay. The correction is stored next to it.

Every station does its job once. The reviewer sees why the ticket got its urgency.

How it is built

Four choices that keep the result explainable.

Each one answers a way the system could let an operator down.

one transactionticketjoboutbox eventrelaySQSretries until SQS accepts

Durable delivery

PostgreSQL commits the triage request and outbox event in one transaction. A relay publishes the event to SQS, and workers use conditional job claims to handle duplicate delivery.

redactedmodelquote in ticketpolicy ID existsthen rules set priorityrulescheck fails or no answer

Constrained AI

The provider receives redacted ticket text and returns structured output. CaseLens validates quoted evidence and policy IDs, calculates priority with application rules, and falls back to deterministic triage after provider or validation failure.

original resulturgency: MEDIUM+ evidence, tracereviewer's correctionurgency:HIGHkept, not rewritten→ correction rate

Human correction

Reviewers can correct urgency without rewriting the original result. Evaluation metrics report category and urgency agreement, correction rate, latency, provider, and model version.

synthetic data only, no real customersprovidertimeoutRETRYABLE_FAILUREinspect the recordreplayCOMPLETED

Inspectable failure

The demo records a synthetic provider timeout as a retryable failure. A reviewer can replay the work and inspect the failure and recovery records without using real customer data.

Verification

Tests cover normal processing, failure, and recovery.

Tests cover duplicate delivery, invalid model evidence, provider failure, operator replay, workspace isolation, queue integration, infrastructure validation, scans, and deployed browser flows.

Application

Java 21Spring BootReact 19TypeScriptTanStack QueryPostgreSQL 16Flyway

AWS

API GatewayLambda + SnapStartS3CloudFrontSQS + DLQEventBridgeCloudWatchSecrets Manager

Delivery

TerraformGitHub Actions OIDCDockerLocalStackTestcontainersTrivyprotected teardown

Engineering decisions

Decisions and trade-offs

What I chose, and what each choice costs.

  1. Modular monolith, with one deliberate asynchronous boundary.

    I kept the first release in one backend so its domain flow, transaction boundaries, and recovery path stay easy to trace. SQS separates triage processing from the request without adding a fleet of services to deploy and operate.

  2. The outbox protects the database-to-queue handoff.

    CaseLens commits the ticket, job, and outbox event in the same PostgreSQL transaction. The relay can retry publication until SQS accepts it, avoiding a stored ticket whose processing message was lost between two separate writes.

  3. Queue messages carry identifiers, not ticket content.

    Workers receive workspace, case, and job identifiers, then reload the current record from PostgreSQL. This keeps the database authoritative, reduces data copied through SQS, and lets the worker apply current validation and redaction rules.

  4. Application rules own priority and fallback behavior.

    The model proposes structured evidence, category, urgency, and actions. CaseLens validates that output and applies deterministic priority and fallback rules, keeping operational decisions reproducible when provider output is invalid or unavailable.

  5. The demo favors low idle cost over constant warmth.

    Lambda and Neon suit an infrequently reviewed portfolio system, but they introduce cold starts and database connection limits. The deployment bounds connection use and accepts a slower first request rather than paying for always-on capacity.