← Almaari case studies

Case study

Redesigning Almaari's AI Garment Ingestion Pipeline

Building a more reliable asynchronous image-processing and AI-enrichment workflow.

Architecture Analysis and Full-Stack EngineeringIn progress

start

Introduction

Almaari's Add Clothes flow had gradually accumulated several separate responsibilities: image background removal, manual AI analysis for meta data extraction, credit accounting, garment persistence, and a background job for meta data enrichment.

The system worked, but the workflow increasingly depended on the user advancing each step manually, while failures across processing services were difficult to recover from cleanly.

I began redesigning the ingestion pipeline around one durable workflow with explicit processing state, safe retries, and automatic garment analysis. The redesign is still being productionized; proposed components and design targets are identified in the writing below.

Overview

What I Changed

The central change is to treat image processing, analysis, and enrichment as one durable workflow centered on the Clothes record. What does this mean? Made uploading an image of your clothes into one async task that allows for safer retries and idempotentcy.

Before

Upload imageCrop backgroundManual AI analysisCredit handlingEditSaveBackground enrichment

After

Upload imageAutomatic cropAutomatic analysisReviewSubmit

The flow

The Current Flow

The current upload experience asks the user to prepare the image, request analysis, and review the result as separate steps.

Current system

The Problem

The current pipeline has user-facing friction, but the deeper issue is that no single layer clearly owns the garment's progress through ingestion.

User-driven orchestration

AI analysis currently requires another explicit user action. The user is responsible for advancing a workflow that the system can already infer from the upload itself.

Billing coupled to ingestion

A standard garment analysis can involve a credit check, credit deduction, the AI request, and a refund when that request fails. That accounting logic makes a basic product workflow harder to retry safely.

Overlapping retry mechanisms

Reliability is spread across frontend submission guards, idempotency keys, MongoDB jobs, setImmediate processing, startup reclaim, and internal worker endpoints. Each mechanism solves a real problem, but together they obscure which layer owns recovery.

Partial failures

Background removal and AI analysis can succeed or fail independently. A successful crop should not need to run again because analysis failed later, but the current workflow does not represent that progress as one explicit checkpointed sequence.

Fragmented workflow ownership

Upload, crop, analysis, persistence, enrichment, and review are not represented by one durable state machine. The redesign moves that responsibility into persisted Clothes state.

Scope

Design Goals & Non-Goals

The redesign has three priorities:

  1. Cleaner architecture. Represent ingestion as one explicit workflow with understandable state transitions.
  2. Better reliability. Use durable task delivery and checkpointed processing so retries resume from completed work.
  3. Lower user-perceived latency. Start analysis automatically after upload instead of waiting for another user action.

Non-goals

  • Rewrite every service.
  • Replace MongoDB.
  • Move every workload to GCP.
  • Introduce Kafka or Pub/Sub.
  • Optimize for millions of uploads.

The goal is to improve the reliability boundary without turning Almaari into a distributed system larger than its current scale requires.

Interactive system map

Explore the Architecture

The system map shows the proposed architecture, current implementation, successful path, and failure and retry paths.

Proposed

The proposed workflow makes persisted Clothes state—not a browser or process—the orchestration boundary.

PERSISTED CLOTHES STATEPROCESSINGCROPPEDREADY FOR REVIEWACTIVEUser / Browser. Client. Select + review.User / BrowserClientSelect + reviewNext.js 16. Vercel. Product frontend.Next.js 16VercelProduct frontendProduct API. Express · Cloud Run. Node API · orchestration.Product APIExpress · Cloud RunNode API · orchestrationCloud Tasks. Google Cloud. Durable task delivery.Cloud TasksGoogle CloudDurable task deliverySELECTEDIngestion Worker. Google Cloud Run. Checkpointed processing.Ingestion WorkerGoogle Cloud RunCheckpointed processingCrop Service. Railway · rembg. Remove background.Crop ServiceRailway · rembgRemove backgroundVision Analysis. Railway · GPT-4o-mini. Garment metadata.Vision AnalysisRailway · GPT-4o-miniGarment metadataAmazon S3. AWS. Original + processed images.Amazon S3AWSOriginal + processed imagesMongoDB Atlas. Persistent state. Source of truth.MongoDB AtlasPersistent stateSource of truth
Primary request / processingRetry / recoveryState / persistence

Workflow ownership

Current vs Proposed

The main change is where workflow ownership lives. Today, the frontend and several background mechanisms collectively move a garment through the system. In the redesign, the Clothes record becomes the durable source of truth and a worker advances it through explicit stages.

ImagePending ClothesS3Cloud TasksWorkerCropAIREADY_FOR_REVIEWEditACTIVE

The redesign moves workflow ownership from the frontend and process lifecycle into persisted backend state.

Persisted orchestration

The Clothes Record Becomes the Workflow

A separate GarmentDraft model would add another lifecycle to create, reconcile, and eventually delete. Because the S3 flow already creates an owned Clothes shell, I decided to use the Clothes record itself as the workflow entity.

CREATE
PROCESSING
READY_FOR_REVIEW
ACTIVE
Proposed failure states:FAILEDREPROCESSING
type ClothesStatus =
  | "PROCESSING"
  | "READY_FOR_REVIEW"
  | "FAILED"
  | "REPROCESSING"
  | "ACTIVE";

type ProcessingStage =
  | "UPLOADED"
  | "CROPPING"
  | "CROPPED"
  | "ANALYZING"
  | "READY_FOR_REVIEW"
  | "FAILED";

Proposed sequence

Request Lifecycle

The user-facing request ends early. Persisted state and durable task delivery carry the remaining processing forward without tying it to the API process lifetime.

1. Create and upload

The user selects an image. Express creates a pending Clothes record and returns a presigned S3 URL. The browser uploads the image directly to S3, then confirms completion with the API.

2. Queue processing

After confirming the upload, Express enqueues PROCESS_CLOTHING:{clothesId}. The ingestion worker receives the task and loads the persisted Clothes state before doing any work.

3. Process and checkpoint

The worker calls the crop service and saves processingStage = CROPPED when it succeeds. It then calls the vision service, persists the returned metadata, and advances the Clothes record to READY_FOR_REVIEW.

4. Review and finalize

The frontend refetches the Clothes record and unlocks editing. The user reviews the metadata, makes any changes, and submits. Finalization advances the garment to ACTIVE.

At-least-once delivery

Retries Are Expected, Not Exceptional

Cloud Tasks provides at-least-once delivery. The worker cannot assume that a task runs only once; duplicate delivery is normal control flow, not an unusual edge case.

Each workflow uses PROCESS_CLOTHING:{clothesId} as its stable identity. Before doing work, the worker reads the Clothes record and decides what remains from the persisted processing stage.

if (clothes.processingStage === "CROPPED") {
  await analyzeGarment(clothes);
}

Request-level idempotency

Protect repeated HTTP create attempts with an Idempotency-Key.

Workflow-level idempotency

Use clothesId + processingStage to determine which work remains.

Finalization idempotency

If the status is already ACTIVE, return the existing Clothes object instead of finalizing it again.

Degraded paths

Failure Recovery

The workflow preserves successful work and keeps a user path available when a dependency remains unavailable.

Crop service outage

A transient crop failure is retried by Cloud Tasks. If retries are exhausted, the garment moves to REPROCESSING and the user can continue with the original image. A delayed recrop can run later without blocking review.

AI service outage

Once cropping succeeds, the CROPPED checkpoint is retained. An AI retry skips cropping and resumes analysis directly. If analysis continues to fail, the user can enter metadata manually.

Browser refresh

The frontend refetches Clothes state and restores the current step. It does not create another workflow or restart completed processing.

Duplicate task delivery

The worker reads the persisted stage and skips completed work. Stage transitions and finalization are designed to be idempotent.

Delayed image reprocessing

If cropping continues to fail, the user should still be able to proceed with the original image. A later crop result may replace a pending image, but it must not silently replace an image the user has already approved.

if (clothes.status !== "ACTIVE") {
  // processed candidate may replace pending image
}

A future interface could offer the improved image as an explicit replacement.

Architecture decision records

Key Architecture Decisions

These decisions keep the reliability boundary explicit without adding infrastructure that Almaari's current scale does not need.

ADR-01

Google Cloud Tasks

Decision: Use Cloud Tasks for durable async delivery.

Why: Managed delivery and retries fit the current scale.

Trade-off: At-least-once delivery still requires idempotency.

ADR-02

Dedicated Cloud Run Worker

Decision: Separate ingestion from the public Express service.

Why: Independent scaling, resource isolation, and a cleaner failure boundary.

Trade-off: Adds one deployable service.

ADR-03

Reuse Clothes

Decision: Do not introduce a separate GarmentDraft.

Why: The S3 flow already creates an owned Clothes shell.

Trade-off: Clothes carries temporary processing state.

ADR-04

Single Checkpointed Task

Decision: PROCESS_CLOTHING handles crop and AI.

Why: Current scale does not justify a task per stage.

Trade-off: The worker owns stage orchestration.

ADR-05

Keep Railway

Decision: Keep crop and AI services on Railway for now.

Why: Migration does not directly solve the reliability boundary.

Trade-off: Revisit if latency, reliability, observability, or overhead becomes measurable.

ADR-06

Automatic Free Analysis

Decision: Standard garment analysis no longer consumes credits.

Why: Removes billing logic from normal ingestion and simplifies retries.

Trade-off: Each upload now has a model inference cost.

ADR-07

Sequential Crop → Analyze

Decision: Crop first, then analyze.

Why: AI receives a cleaner image and the pipeline stays easy to reason about.

Trade-off: Potentially higher latency than parallel analysis.

Deliberate constraints

Rejected & Deferred Alternatives

MongoDB as the primary queue

It reuses an existing datastore and resembles Almaari's current job handling.

Why I deferred it: Polling, delivery semantics, and abandoned-job reclaim would remain application-owned.

Trade-off: It avoids another provider but creates more queue infrastructure to maintain.

Processing inside Express

It avoids another service and is straightforward to deploy.

Why I deferred it: Long-running work would remain coupled to public request capacity and the API process lifecycle.

Trade-off: There are fewer deployables, but a weaker failure and scaling boundary.

One task per processing stage

Separate crop and analysis tasks would provide granular isolation.

Why I deferred it: The current upload volume does not justify the extra task definitions and orchestration.

Trade-off: Retries become more granular at the cost of a more complex workflow.

Move every service to GCP immediately

One provider could simplify networking and observability.

Why I deferred it: A migration does not directly solve workflow ownership or retry behavior.

Trade-off: Operations may become simpler later, but the near-term migration cost is high.

Keep manual AI analysis

It avoids inference for uploads the user may abandon.

Why I deferred it: It preserves a second user action, billing coupling, and a delayed processing start.

Trade-off: Model usage is lower, but user effort and perceived latency remain higher.

Introduce a separate GarmentDraft

A draft entity could isolate temporary ingestion state.

Why I deferred it: The existing S3 flow already creates an owned Clothes shell.

Trade-off: The conceptual boundary is cleaner, but another lifecycle must be reconciled and deleted.

Right-sized infrastructure

Cost & Scale

Almaari currently has more than 50 users and sees roughly 20–30 garment uploads per day. At that scale, the priority is operational clarity and failure recovery rather than maximum throughput.

Cloud Tasks volume is small, the worker can scale down while idle, and there is little reason to migrate functioning Railway services solely for architectural neatness. AI inference is more likely to dominate variable cost than task delivery.

Evidence

Metrics

Current scale

  • More than 50 users.
  • Roughly 20–30 garment uploads per day.

Design targets

  • Under five seconds for normal garment processing.
  • Safe duplicate task delivery.
  • Partial retry from persisted checkpoints.
  • Manual fallback after AI failure.
  • No credit dependency for standard garment analysis.

What I plan to measure after rollout

  • P50 and P95 processing latency.
  • Crop and AI latency.
  • Queue delay and retry rate.
  • Crop and AI failure rates.

Implementation

Productionization Status

The redesign remains in progress. The lists below distinguish production behavior from planned work.

Already in place

  • S3 production image path
  • Clothes shell creation
  • Railway crop service
  • Railway vision service

In progress

  • New processing state machine
  • Cloud Tasks integration
  • Automatic free analysis UX

Still to validate or implement

  • Dedicated Cloud Run ingestion worker
  • Checkpointed retry
  • Latency benchmarking