DEV Community

WinslowKnight8469
WinslowKnight8469

Posted on

Property-Tour Videos — Selecting Practical Generation, Storage, and Delivery Boundaries

Short answer: generate a property-tour video once per meaningful version, keep the durable result behind an immutable asset key, and make the delivery tier cache that asset without gaining authority to regenerate it. That boundary usually wins because retries, edits, and viewer traffic have different SLOs and radically different cost curves. It is not universal: a tiny internal preview tool can stay synchronous, while a system that personalizes every playback may need a session-scoped generation path.

The failure I use in a capacity review is deliberately bounded. Suppose a B2B SaaS product accepts a listing prompt, renders a 45-second promo video, and then serves it to agents and prospective buyers. A campaign launches, requests jump from 20 to 400 per minute, and viewers repeatedly ask for the same finished version. If a cache miss can call generation, delivery demand becomes render demand. The queue grows, fresh jobs compete with duplicate jobs, and an apparently generous playback SLO starts spending the generation budget. No vendor failure is required. The architecture did exactly what it was allowed to do.

That is the invariant: a read path must not silently become a create path.

Why generation and delivery need separate failure budgets

Generation is a workflow. It consumes a prompt, listing data, media inputs, and a declared output profile; it can take time, it can be retried, and it produces a new version. Delivery is a read service. It accepts an asset identity and a byte range, then returns an already-published representation. Combining them looks convenient in a demo because one URL appears to “make the video available,” but the shared boundary hides the only question an on-call engineer will care about later: which class of work is permitted to consume scarce render capacity?

I would give the two paths different service-level objectives. The generation SLO should describe queue admission, completion, and freshness for a submitted version. The delivery SLO should describe availability and latency for a published asset. Don't let a delivery retry count as a generation retry. Don't let an expired edge object erase the durable publication record either. Those rules make an error budget actionable: a team can slow new admissions when render backlog rises without degrading playback of completed tours.

The object identity matters more than the hostname. A useful key is derived from immutable inputs such as listing revision, normalized prompt revision, source-media manifest, and output profile. The public listing can point to the current key, but the bytes under that key do not change. A corrected bedroom photo creates another key. This costs some metadata and leaves old objects to a retention policy, yet it prevents cache invalidation from becoming the coordination protocol for publication.

One subtle trap remains. “Same prompt” does not necessarily mean “same generation request” if the source manifest or output profile changed. I am not sure a globally deterministic key is possible for every generation engine; the engine's documented determinism and version controls would settle that. The conservative design is still available: assign an idempotency key to the submitted workflow, record its resolved inputs, and publish exactly one durable asset version from that workflow.

How should property-tour videos split generation and delivery boundaries?

Put an explicit publication record between the two planes. The generation worker may write an object and then atomically mark its version publishable. The delivery service may resolve a public tour identifier to that published version, but it cannot enqueue work. A missing published version returns a domain state such as processing or not_published; it does not improvise a render. This is less magical, which is good.

A practical request flow looks like this:

  1. The control plane validates the prompt, source-media manifest, and output profile, then admits or rejects a generation job according to queue capacity.
  2. A worker writes the result under a versioned, immutable key and verifies the completed object before publication.
  3. The publication record changes from processing to published and exposes that key to the read path.
  4. The delivery tier serves the published object through caches, including byte-range requests where the chosen media format and client behavior call for them.
  5. An edit creates another version. Existing viewers can continue reading the old object until the pointer changes; no partial replacement leaks through.

This boundary also clarifies error handling. Invalid input is rejected before admission. Duplicate submission resolves to the existing workflow. Capacity pressure produces an admission result that a client can back off from. A delivery miss can retry its storage read within a tight budget, but it can't cross the line and manufacture new bytes. Short rule. Big effect.

No regeneration.

Media compatibility belongs in the output-profile decision, not in an incidental worker default. A file extension alone does not describe every compatibility property; container, video codec, audio codec, and browser support all matter. The MDN media formats guide is a useful starting point for that matrix. Test the actual target browsers with the actual encoded profile, including seeking and range behavior, before promoting the profile. For a property-tour product, that test should cover the agent's preview surface and the buyer-facing playback surface separately because they may have different client populations.

Storage and cache cost should be modeled per published version

Storage bills accumulate by retained bytes and time; delivery costs follow reads and transferred bytes; generation cost follows admitted work. Those dimensions should stay separate in the model even if one provider later sends a combined invoice. Otherwise a cache improvement can appear to “fix video cost” while retained abandoned drafts continue to grow, or an aggressive retention rule can look efficient while forcing legitimate regeneration.

Here is a hypothetical capacity sheet, not a benchmark. Let one output profile average 18 MB, let a listing produce three candidate drafts and one published version, and let the product process 10,000 listings in a month. That is 720 GB of newly written video before replicas or provider-specific accounting: 18 MB × 4 × 10,000. If only the published result must remain for the contractual retention period, draft expiry is a first-order control. If every draft is an auditable customer record, deleting three quarters of the bytes is not an available optimization. Policy comes first.

Cache planning needs the same honesty. Model requests per published version, bytes transferred per successful playback, range-request behavior, hit ratio by geography, and origin requests after expiry. A single global hit ratio is too flattering for a new campaign: cold objects and hot objects behave differently. I prefer a small table with low, expected, and high inputs, then a sensitivity check that doubles video size and halves the hit ratio. If that breaks the monthly envelope or origin SLO, the system has no capacity margin; it has a forecast that depends on being lucky.

Decision Buy a managed capability when Build or self-host when Cost or on-call catch
Generation queue The team wants bounded admission and managed worker operations Scheduling policy is a core differentiator Owning retries and fairness creates pager work
Durable object storage Standard object semantics fit retention and access needs Data placement or specialized storage behavior is mandatory Egress and lifecycle policy can outweigh raw capacity price
Delivery cache Traffic is geographically dispersed or bursty Audience is small, controlled, and near the origin Cache misses, invalidation, and observability still need ownership
Media processing A fixed set of tested profiles is sufficient Encoding behavior is product-critical More profiles multiply storage, testing, and support load

The table is not a scorecard. Managed services can reduce routine operations while increasing switching work; self-hosting can make placement and scheduling explicit while transferring upgrades, capacity, and incident response to the platform team. I would require an exit test either way: can the publication metadata be exported, can immutable objects be copied while preserving identity, and can a second delivery implementation serve the same keys? Lock-in is measurable when the migration unit is defined.

How can admission control keep playback demand out of the render queue?

The preventative code path is small enough to review. This Go example keeps publication lookup read-only and gives generation admission a separate interface. The numbers are configuration inputs for an illustrative deployment, not universal limits.

package tours

import (
    "context"
    "errors"
)

var (
    ErrProcessing   = errors.New("tour is processing")
    ErrNotPublished = errors.New("tour is not published")
    ErrQueueFull    = errors.New("generation queue is full")
)

type Publication struct {
    TourID     string
    Version    string
    ObjectKey  string
    Status     string
}

type PublicationStore interface {
    Current(ctx context.Context, tourID string) (Publication, error)
}

type GenerationQueue interface {
    Depth(ctx context.Context) (int, error)
    Submit(ctx context.Context, idempotencyKey string, request GenerateRequest) error
}

type GenerateRequest struct {
    TourID         string
    ListingVersion string
    PromptVersion  string
    MediaManifest  string
    OutputProfile  string
}

func ResolveForDelivery(ctx context.Context, store PublicationStore, tourID string) (string, error) {
    pub, err := store.Current(ctx, tourID)
    if err != nil {
        return "", err
    }
    if pub.Status == "processing" {
        return "", ErrProcessing
    }
    if pub.Status != "published" || pub.ObjectKey == "" {
        return "", ErrNotPublished
    }
    return pub.ObjectKey, nil
}

func AdmitGeneration(
    ctx context.Context,
    queue GenerationQueue,
    maxQueued int,
    idempotencyKey string,
    request GenerateRequest,
) error {
    depth, err := queue.Depth(ctx)
    if err != nil {
        return err
    }
    if depth >= maxQueued {
        return ErrQueueFull
    }
    return queue.Submit(ctx, idempotencyKey, request)
}
Enter fullscreen mode Exit fullscreen mode

The important property is negative: ResolveForDelivery has no queue dependency. A future refactor cannot regenerate on a miss without changing the interface and triggering an architectural review. I'd also emit separate metrics for admission rejections, queue age, generation completion, publication lag, delivery latency, cache outcome, origin bytes, and retained bytes by lifecycle class. A single “video success” counter would bury the boundary again.

Keep those graphs separate.

Deployment should preserve it. Roll out a new output profile behind an explicit profile identifier, generate test fixtures, verify playback on the supported client matrix, and then admit a limited share of new jobs. Existing published keys remain readable. Rollback means selecting the prior profile for new work, not rewriting objects already promised to viewers.

When is this boundary the wrong choice?

The catch is coordination overhead. A two-plane design needs workflow state, publication metadata, lifecycle rules, and separate dashboards. It is not suitable when every video is a disposable, user-specific stream that cannot be reused or cached; in that case, session-scoped generation and delivery may be one bounded pipeline, with strict concurrency limits. It may also be excessive for an internal preview used by a handful of operators, where a synchronous job and short retention meet the real SLO and the team accepts manual recovery.

Stick with a simpler synchronous boundary when the maximum request rate is known, the audience is controlled, generation completes inside the request budget, and duplicate work cannot threaten other tenants. Revisit the split before public launch, multi-tenant growth, or geographic delivery changes those assumptions. Conversely, don't build a global delivery layer merely because video is involved; if viewers are concentrated near one origin and traffic is predictable, another cache tier can add cost and failure modes without buying meaningful SLO margin.

The decision rule is plain: separate generation from delivery when a reusable published version exists and playback demand can outgrow render capacity. Then make the separation visible in identity, interfaces, budgets, metrics, and ownership. A diagram alone won't hold the line.

References

Top comments (0)