Short answer: choose a product-image pipeline that withholds search tags until every required moderation check has reached an explicit terminal state, keeps the original upload separate from derived catalog assets, and can replay policy decisions without asking a seller to upload the photo again.
The tempting design is upload, resize, auto-tag, publish. The operational design is less tidy: ingest an immutable source, validate what can actually be decoded, derive display assets, run moderation and tagging as separate decisions, then expose only the versions whose evidence satisfies the current catalog policy. That extra state matters more than a prettier dashboard. At 3 a.m., the useful question isn't “is image processing green?” It is “what page fired, which listings became searchable, and what evidence let them through?”
How should marketplace product images enter a consistent catalog search pipeline?
Start by defining “consistent” as a release rule, not a visual adjective. Two catalog photos may have identical dimensions and compression while one has complete moderation evidence and the other has only a plausible auto-tag. If both reach search, the pixels are consistent but the system isn't.
A bounded incident scenario makes the distinction concrete. A B2B marketplace accepts a seller's new product photo. The image decoder accepts it, a display rendition is produced, and the tagging worker emits red, linen, and shirt. The moderation worker has not recorded a decision. A search indexer that listens only for tags.ready publishes the listing under those terms. Later, a policy review finds that the item should never have been discoverable in that tenant's catalog. Nothing had to crash; every component could have reported healthy while the release contract was wrong.
The invariant is blunt: searchability requires complete moderation coverage, not merely successful tagging. The indexer should consume a release decision that joins asset identity, policy identity, moderation state, and tag state. It should never infer approval from the absence of a rejection.
No inference.
This is where dashboards mislead. A chart can show 99% completed work while the remaining 1% is concentrated in one customer, one import batch, or one decoder path. Use per-asset state and queryable reason codes. If an image can't be decoded under the accepted media formats, reject it before fan-out; MDN's media format guide is a useful starting point for understanding that containers, codecs, and browser support are separate concerns rather than interchangeable labels.
Keep it boring.
A postmortem starts at the publication decision
The first timeline event worth examining is not the alert. It is the state transition that made a product discoverable. Work backward from that point: which source digest was indexed, which policy version applied, which moderation checks were required, which checks completed, and which tag set was attached? A generic “job succeeded” event answers none of those questions.
I would write the postmortem around three clocks. The ingest clock begins when the source is accepted. The analysis clock covers moderation and tagging, which can finish in either order. The publication clock begins only when the release evaluator sees all required evidence. Separating those clocks prevents a slow tagger from looking like a moderation outage, and it prevents a fast tagger from silently outrunning a required safety decision.
One useful event record is deliberately small:
| Field | Why it belongs in the incident timeline |
|---|---|
asset_digest |
Joins the source, renditions, decisions, and indexed document without trusting a mutable filename |
tenant_id |
Shows whether exposure is isolated or crosses marketplace customers |
policy_version |
Explains which checks were required at the moment of release |
moderation_state |
Distinguishes pending, accepted, rejected, and review-required outcomes |
tag_state |
Separates missing labels from a blocked image |
release_reason |
Makes the final allow or deny decision auditable |
Do not collapse review_required into rejected. They drive different operations: rejection is terminal for that policy evaluation, while review creates owned work that needs an age, a queue, and an escalation rule. Likewise, “no record” is not a state. Treat it as missing evidence and fail closed for search publication.
The page should fire on the user-visible contract: an asset was published without the full required decision set, or the oldest eligible asset has waited beyond the service objective. CPU, queue depth, and worker errors remain diagnostic signals. They are poor substitutes for the catalog outcome because a quiet queue can mean either that all work finished or that no work was enqueued.
Make policy coverage a data model
Moderation coverage is a set comparison. For each policy version, store the required check identifiers. For each source digest, store completed decisions with their check identifier, decision, evaluator revision, and timestamp. Coverage is complete only when every required identifier has a terminal decision. The evaluator then combines those decisions with the marketplace's release rule.
This model handles policy changes without pretending old decisions were made under new rules. When a policy adds a check, previously published assets can move into a controlled re-evaluation cohort; the source digest lets the system reuse the accepted original, while a new decision set records the new policy version. Whether already visible listings remain visible during that replay is a product policy choice, and I'm not sure there is one safe default for every marketplace. Tenant contracts, the severity of the new check, and the review team's capacity should decide it.
There is a practical trap here — mutable tags. If a tagging worker overwrites a row while the search indexer reads it, an incident review may know that a listing was released but not which labels were released. Store tag sets as versioned outputs and put the selected version in the release record. A correction becomes a new decision, not an edit to history.
The same rule applies to image derivatives. Preserve a content-addressed source and treat crops, thumbnails, and display encodings as derived artifacts. The accepted input formats and output formats should be explicit at the boundary, with browser delivery choices checked against current format support rather than guessed from a file extension. This separation also keeps moderation scope legible: the policy can say whether checks apply to the original, a normalized rendition, or both.
This data model creates a crisp negative assertion: there must be no published release whose required-check set exceeds its completed terminal-check set. Run that assertion continuously and during deployment. Aggregate completion percentages can remain on the dashboard, but they don't get the final vote.
Gate indexing with one deterministic decision
The preventative code path should be a pure decision surrounded by durable I/O. Workers may retry. Events may arrive twice or out of order. Given the same policy, moderation decisions, and tag result, however, the gate must return the same answer and reason. That makes replay useful instead of risky.
The following Go example omits storage and transport on purpose. It shows the contract between analysis and indexing without tying the catalog to a particular queue, image library, or hosted service.
package release
import (
"fmt"
"sort"
)
type Decision string
const (
Accepted Decision = "accepted"
Rejected Decision = "rejected"
ReviewRequired Decision = "review_required"
)
type ModerationResult struct {
CheckID string
State Decision
}
type Evaluation struct {
Publish bool
Reason string
}
func Evaluate(required []string, results []ModerationResult, tagsReady bool) Evaluation {
terminal := make(map[string]Decision, len(results))
for _, result := range results {
switch result.State {
case Accepted, Rejected, ReviewRequired:
terminal[result.CheckID] = result.State
}
}
missing := make([]string, 0)
for _, checkID := range required {
state, ok := terminal[checkID]
if !ok {
missing = append(missing, checkID)
continue
}
if state != Accepted {
return Evaluation{Reason: fmt.Sprintf("moderation_%s:%s", state, checkID)}
}
}
if len(missing) > 0 {
sort.Strings(missing)
return Evaluation{Reason: fmt.Sprintf("moderation_incomplete:%v", missing)}
}
if !tagsReady {
return Evaluation{Reason: "tags_pending"}
}
return Evaluation{Publish: true, Reason: "policy_satisfied"}
}
Persist the returned reason with the release attempt. A moderation_incomplete count is actionable only when it can be sliced by policy version, tenant, age, and check identifier; otherwise the on-call engineer knows there is a gap but cannot tell whether it is a normal five-second race or a batch that has been stranded for hours.
Deployment deserves the same skepticism. Shadow the new policy evaluator against recorded inputs before it controls publication, compare old and new decisions by reason, and canary the gate for a bounded tenant cohort. The rollback unit should be the policy version and evaluator revision, not a manual edit to individual listings. For schema changes, prove that both the previous and next application versions can read the release record during the rollout window.
Choose the pipeline by failure ownership
There are three defensible shapes. A synchronous pipeline performs decode, moderation, tagging, and release inside the upload request. It is easy to reason about for small, tightly bounded inputs, but it couples seller latency and availability to every analysis step. Stick with it when the full decision reliably fits the request budget and delayed publication would add complexity without operational value. An asynchronous orchestrated pipeline persists the source, schedules independent analysis, and invokes a release evaluator after state changes. It makes retry and replay explicit, and it lets moderation and tagging scale independently. The catch is that it introduces queues, idempotency keys, reconciliation, and a larger state space. It is not suitable when the team cannot own those controls or when moderation must provide an immediate answer before accepting the upload. A hybrid pipeline performs cheap structural validation synchronously, then handles policy analysis and search release asynchronously. That is often the most natural fit for a B2B SaaS media library because acceptance and discoverability are separate promises, but “hybrid” is not a free pass: the UI and API must expose the distinction between stored, under review, searchable, and rejected. Otherwise support staff will interpret an accepted upload as a published listing.
Choose among them with incident questions, not feature counts. Can the team identify every asset released under policy version 12? Can it replay a decision without mutating the original? Can it prove that no tag reached search before required moderation completed? Can it drain or pause publication without stopping uploads? If the answer is a dashboard screenshot rather than a query over durable state, the pipeline is not ready.
The final acceptance test is equally plain. Feed the gate results in every order, omit each required check once, duplicate events, change a policy version, and replay the same inputs. Publication should occur once, only after complete accepted moderation evidence and ready tags. Then page on violations of that invariant.
References
- MDN Web Docs, “Media container formats (file types)”: https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats
Top comments (0)