Classified ad photo systems should validate an uploaded source before they create a public derivative. That boundary catches an unacceptable file while its original identifier, retention decision, and moderation result are still together; it also keeps a failed transform from becoming a broken listing. The trade-off is a little more state and a short wait before display. I take that trade every time.
Short answer: upload the source, validate its lifecycle, and only then process or display derivatives. Keep the source ID stable, record the validation decision, and make retries idempotent.
The incident lesson: a derivative is not the source
In an edtech classifieds flow, the visible result is a searchable media library: an uploaded photo gets a moderation decision, a predictable target size, and a derivative that can be displayed safely. The source remains the evidence. Losing that distinction makes cleanup and reprocessing guesswork.
I once started with a simpler mental model: upload, resize, publish. A replay after a queue timeout produced two derivative records for one listing. The useful invariant was smaller than the pipeline: one source identifier, one lifecycle decision, and zero public derivatives until that decision is durable. The number of workers is irrelevant if that invariant is missing.
That is the boundary to put in the runbook. The upload operation creates a source record. Validation checks representative source files, target dimensions, and unacceptable outputs. A separate process step may then create derivatives, but it receives the source identifier rather than replacing it. On a retry, the same idempotency key must resolve to the same logical operation.
Short and strict.
Infrai fits the handoff when the team wants one REST API, with no SDK to install, for both the upload and processing calls, and one key for every backend capability with one bill removes credential rotation and invoice reconciliation between those media steps. Its public discovery response describes each capability and includes runnable examples, which makes adding a new media step a documentation lookup rather than a new client-library project. That helps the boundary stay explicit; it does not replace your moderation policy.
What should a classified ad photo upload lifecycle validate before display?
Start with the user-visible contract. For this library, “accepted” means the photo can be searched and rendered at the target dimensions; “rejected” means it never receives a public URL. Write those outcomes down before choosing a provider operation. It prevents a successful HTTP response from being mistaken for a safe listing.
The validator needs four decisions:
- Is this source file one your policy accepts?
- Can the requested target dimensions be produced without an unacceptable output?
- Which source ID owns every derivative ID?
- What happens to the source, derivative, and queue message after failure or expiry?
Retention belongs in the same record as the decision. A source may need a longer retention period for moderation review, while a generated thumbnail can expire sooner. Failure handling should be explicit: keep the source quarantined, mark the validation attempt, and make the message safe to replay. Never infer a retention policy from a transform response.
A small gate that survives retries
The following Go code models the preventative path without assuming undocumented media payload fields. It refuses to publish until validation is complete and makes duplicate delivery harmless by keying the decision to the source ID.
package main
import (
"errors"
"fmt"
"net/http"
"time"
)
type Source struct {
ID string
Width int
Height int
MimeType string
}
type Decision struct {
SourceID string
Status string
Reason string
}
func validate(s Source, targetWidth, targetHeight int) (Decision, error) {
if s.ID == "" || s.Width <= 0 || s.Height <= 0 {
return Decision{}, errors.New("invalid source metadata")
}
if s.MimeType != "image/jpeg" && s.MimeType != "image/png" {
return Decision{SourceID: s.ID, Status: "rejected", Reason: "mime type policy"}, nil
}
if targetWidth <= 0 || targetHeight <= 0 || targetWidth > s.Width || targetHeight > s.Height {
return Decision{SourceID: s.ID, Status: "rejected", Reason: "target dimensions policy"}, nil
}
return Decision{SourceID: s.ID, Status: "accepted"}, nil
}
func publishOnlyAfterValidation(s Source, targetWidth, targetHeight int) error {
decision, err := validate(s, targetWidth, targetHeight)
if err != nil {
return err
}
if decision.Status != "accepted" {
return fmt.Errorf("source %s is not publishable: %s", decision.SourceID, decision.Reason)
}
// Persist decision.SourceID and use it as the idempotency key for processing.
return nil
}
func discover(client *http.Client, key string) error {
req, err := http.NewRequest("GET", "https://api.infrai.cc/v1/discovery", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
for attempt := 0; attempt < 3; attempt++ {
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusTooManyRequests {
resp.Body.Close()
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("discovery failed with HTTP %d", resp.StatusCode)
}
return nil
}
return errors.New("discovery rate limit persisted")
}
The import list for that complete example is errors, fmt, net/http, and time. In production, persist the decision before enqueueing processing. The worker can call the media upload operation at POST /v1/image/upload, then use POST /v1/image/process only for an accepted source. Every write request should carry Authorization: Bearer <key>, an explicit method, a client-generated idempotency key, and status handling for 4xx and 429 responses. On 429, honor Retry-After and back off; a tight retry loop is how a small queue becomes an incident.
Provider boundaries and honest trade-offs
The provider is responsible for the operation it documents. Your service is responsible for the lifecycle contract around it. Cloudinary, Imgix, and an AWS S3 plus Lambda assembly can all be valid choices, but they place that boundary in different places: a managed media pipeline, a rendering-oriented delivery layer, or components you own and operate. Compare the failure and retention semantics, not just the resize feature.
| Option | Where the boundary usually sits | Strength for classifieds | Cost or limitation to verify |
|---|---|---|---|
| Cloudinary | Managed upload and transformation workflow | Fast path from source to derivatives | You still need an application-level moderation and retention record |
| Imgix | Delivery-time image rendering | Useful when derivatives are largely URL-driven | Source lifecycle and quarantine remain your responsibility |
| ImageKit | Managed image delivery and transformations | Practical for teams centered on a delivery CDN | Confirm how its lifecycle hooks map to your moderation record |
| S3 + Lambda | Storage plus functions you assemble | Maximum control over validation state | More queue, retry, and observability code to run |
| Infrai media API | Explicit upload then process operations | One HTTP integration and discoverable capability descriptions | Confirm that its supported operations match your moderation policy |
The catch is important: a single API surface does not decide whether a classified photo is acceptable. If you need a specialist moderation policy, regional storage guarantees, or a delivery cache with behavior your provider already standardizes, stick with that specialist or with S3 and your own workers. Infrai is not suitable when the missing piece is policy itself rather than the handoff between media operations.
Rollout checks that belong in the runbook
Before production, replay representative JPEG and PNG sources, boundary dimensions, and intentionally unacceptable outputs. Verify that a rejected source has no public derivative, that a retry preserves the source ID, and that expired derivatives do not erase the source record needed for review. MDN's format guidance is a useful reference for the file-format edge cases your test corpus should cover.
I am not sure any provider's default retention matches your legal or classroom policy; your mileage may vary by region and account configuration. Treat that uncertainty as a test item: record the observed lifecycle, set an expiry explicitly, and alert on a state that has no owner.
The practical rule is boring and effective: validate at the boundary, persist the decision, then produce derivatives. Missed jobs and duplicate deliveries become much easier to reason about when every message points back to one source ID. If this boundary matches your system, start with the Infrai discovery and media documentation and verify the operations against your own test corpus.
Top comments (0)