← System Design
AI Systems

Design an Image Generation Service (Midjourney)

In simple words

A photo studio with a waiting room: you hand in your request and get a token, artists (GPUs) take requests in order (priority members first, but everyone moves), and you're called when your pictures are ready. If a request fails, you're not charged.

Problem Statement

Design a text-to-image service like Midjourney or DALL·E: users type a prompt, and the system generates images on GPUs and shows them when ready. Each job takes seconds to tens of seconds of expensive GPU time, so the design is an asynchronous job system with queues, fair scheduling, credits, and delivery of results.

Functional Requirements

Non-Functional Requirements

Core Entities

API Design

POST /v1/jobs { "prompt": "a lighthouse at dawn, watercolor", "n": 4, "size": "1024x1024" }
  Idempotency-Key: ...
  -> 202 { job_id, status: "QUEUED", position: 37 }
GET  /v1/jobs/{id}                  -> { status, progress, images: [url...] }
GET  /v1/jobs/{id}/events (SSE)     -> queued -> running 40% -> done
GET  /v1/users/me/gallery?cursor=...

High-Level Design (HLD)

Asynchronous image generationUserJob API(safety,credits)Fast queue(paid)Relaxed queue(free)Scheduler(weighted, fair)GPU workersObject storageCDNpromptbatchesstatus events
Jobs wait in tiered queues, a fair scheduler batches them onto GPUs, and results are delivered from storage via the CDN.

Low-Level Design (LLD)

Job lifecycle with credit reservationQUEUED(credits reserved)RUNNING(GPU lease)SUCCEEDED(charge settled)FAILED / BLOCKED(refund)worker leasesafter N retrieslease expired
Credits are reserved on submit and settled or refunded at the end, all as ledger entries tied to the job id.

Job lifecycle and credits

submit:  reserve credits (ledger entry -4)       -> QUEUED
worker:  lease job (visibility timeout) -> RUNNING -> progress events
success: upload images, safety check passes      -> SUCCEEDED (reservation becomes a charge)
failure: retry up to N (new lease on another GPU) -> FAILED -> refund (ledger +4)
blocked: output safety check fails                -> BLOCKED -> refund per policy

Fair scheduling

Workers and failure

Follow-up Questions

Why asynchronous instead of a request that waits for the image?
Generation takes seconds to minutes and depends on queue length; holding HTTP requests that long ties up connections and fails on flaky networks. Submit → poll/stream status → fetch result is robust and lets you show queue position.
How do you avoid charging for failed jobs?
Reserve credits on submit and settle only on success (or refund on failure), all as ledger entries tied to the job id, so retries and duplicate events can't charge twice.
How would you cache results?
Exact repeats (same prompt, params and seed) can return stored images; otherwise generation is nondeterministic by design and caching helps little. Caching the text-encoder output for popular prompts can save a little compute.

Related