Short answer: process a clean master at upload, then create audience-specific derivatives on demand behind an idempotent job queue. Keep watermark policy and output format in versioned metadata, so a missed job or duplicate delivery can be repaired without touching the original photo.
An edtech brand portal has a deceptively sharp edge: a product team uploads a photo of a robotics kit, while a teacher, a partner, and a public catalog each need a different file. The teacher may need a small WebP with no watermark. A partner may need a JPEG with a visible mark. The public catalog may require a size-limited PNG. Treating those requests as one conversion creates accidental exposure and hard-to-reproduce output.
Start with an immutable master and a policy decision
At upload, validate the file, store the original bytes, and record a content hash. Background removal for the product photo belongs in this stage if it is part of the portal's editorial workflow. The result is a new asset version; it never overwrites the uploaded master. That distinction matters during a rollback: you can retire a derivative while retaining evidence of what the editor approved.
The distribution request should carry an audience, a policy version, and an output contract. An output contract is more precise than a filename. It states the media type, maximum dimensions, compression setting, alpha-channel requirement, watermark placement, and whether a transparent background is allowed. Store this contract with the job, not only in application configuration. Configuration changes then produce a new policy version instead of silently changing old links.
A useful key is assetHash/audience/policyVersion/format. It makes the operation naturally idempotent. If a queue redelivers the same message, the worker can see that the exact derivative already exists and acknowledge the message without writing a second object. I learned to insist on this after a scheduler replay produced duplicate catalog records with different URLs. The files were identical, but downstream caches treated them as separate assets, so editors saw two “approved” images and deleted the wrong one during cleanup. The replay also made our daily count look healthy because both records emitted success events. The actual signal was the mismatch between unique source hashes and published derivative rows. Since then, the manifest key is the primary join field in dashboards and repair scripts, and a duplicate delivery is counted as a normal queue outcome rather than an incident.
Keep it boring.
How should audience watermarking and format conversion be scheduled?
Use two paths. Generate a small set of predictable derivatives at upload when the portal promises them in the publish flow. Generate uncommon audience and format combinations on demand, with the request placed on a durable queue. This keeps upload latency bounded while preserving a clear signal for expensive or rare work.
The queue message needs a deduplication key and a deadline. The worker claims a message with a lease, reads the immutable source, applies the policy, writes to a temporary object, verifies the bytes, and then publishes by an atomic rename or finalized object key. A lease expiry is expected control flow, not a reason to mutate the source. Retries should use exponential backoff with a cap, and a dead-letter queue should retain the original message plus the last error category.
Here is a deliberately small Go sketch. The interfaces represent storage and image tooling; they keep the scheduling behavior testable without coupling it to a vendor SDK.
type DerivativeKey struct {
AssetHash string
Audience string
PolicyVersion string
Format string
}
func Process(ctx context.Context, job Job, store Store, transform Transformer) error {
key := DerivativeKey{job.AssetHash, job.Audience, job.PolicyVersion, job.Format}
if ok, err := store.Exists(ctx, key); err != nil {
return fmt.Errorf("check derivative: %w", err)
} else if ok {
return nil // duplicate delivery; acknowledge safely
}
src, err := store.OpenMaster(ctx, job.AssetHash)
if err != nil {
return fmt.Errorf("open master: %w", err)
}
defer src.Close()
tmp, err := store.CreateTemp(ctx, key)
if err != nil {
return fmt.Errorf("create temp: %w", err)
}
defer tmp.Abort()
if err := transform.Apply(ctx, src, tmp, job.Contract); err != nil {
return fmt.Errorf("transform: %w", err)
}
if err := tmp.Verify(ctx, job.Contract); err != nil {
return fmt.Errorf("verify derivative: %w", err)
}
if err := tmp.Commit(ctx, key); err != nil {
return fmt.Errorf("commit derivative: %w", err)
}
return nil
}
The important behavior is the order, not the placeholder transformer: check for the key, read the master, write privately, verify, then publish. A crash before commit leaves no public derivative. A crash after commit is harmless on replay.
That ordering is the runbook.
What do format contracts and watermark rules need to cover?
Format conversion is a compatibility decision. Browser support differs by media type, and a transparent product cutout cannot be represented in formats that lack an alpha channel. Use the media type as the source of truth and set the HTTP Content-Type header from the finalized file, rather than trusting a user-supplied extension. The MDN media formats guide is a practical reference for browser-oriented choices.
Watermarks are policy, not decoration. Define the audience's rights first: internal review, classroom download, partner campaign, or public listing. Record the mark's text or image identifier, opacity, placement anchor, minimum size, and policy version. Keep a no-watermark path explicit; otherwise a later default can leak into a private derivative. For accessibility and downstream editing, preserve a clean master and make the mark reversible by deleting only the derivative.
A manifest beside each derivative should include the source hash, transform version, dimensions, byte size, media type, and policy decision. It gives support staff a way to answer “why did this partner file have a mark?” without opening the binary. It also makes cache invalidation mechanical: a policy change creates a new key and leaves the old version available until its retention window ends.
Verify the pipeline like a production incident
Metrics should expose intent and outcome separately: upload-to-master latency, queued jobs by audience, age of the oldest queued job, transform duration, verification failures, duplicate-delivery count, and dead-letter count. Alert on a sustained increase in queue age or verification failures, not on one slow image. Include the asset hash and policy version in structured logs, but avoid logging signed download URLs.
Tests need more than a happy-path screenshot. Property tests can assert that processing the same key twice yields one published object. Contract tests can assert that a PNG request has the expected alpha behavior and that response headers match the bytes. Run a failure-injection test that kills the worker after verification but before commit; the retry must publish exactly one derivative. A second test should expire the lease while the first worker is still running, proving that the commit operation remains conditional.
Rollback is a policy operation. Stop issuing new jobs for the affected policy version, let in-flight leases finish or expire, and switch reads to the last known-good derivative version. Do not delete masters as part of rollback. After the queue is drained, inspect the manifest for partial or mismatched outputs and requeue only those keys.
The catch is that on-demand conversion is a poor fit for a launch path that promises a file in the same request, and upload-time generation is wasteful when audiences and formats are mostly unknown. Stick with upload-time derivatives for a small, contractual set; use on-demand jobs when the matrix is large or changes often. Your mileage may vary with image dimensions and storage latency, so measure queue age and publish latency in your own portal before setting an SLO.
A vendor-neutral design also keeps migration practical. A worker that speaks ordinary object storage semantics, standard HTTP headers, and a narrow transformer interface can move between image libraries or hosting environments without rewriting policy code. The trade-off is operational ownership: your team must maintain the queue, leases, observability, and retention rules. A managed media service may reduce that work, but it can impose limits on custom watermark logic, audit fields, or where bytes are processed.
Top comments (0)