Draft and Publishing Service Low-Level Design: State Machine, Scheduled Publish, and Preview

A draft and publishing service manages the lifecycle of content from initial creation through approval, scheduled release, and potential rollback. It must enforce clear state transitions, execute scheduled publishes at exact times, provide preview access for stakeholders before publication, and allow safe rollback without data loss. This post covers the full design.

Requirements

Functional Requirements

  • Content exists in states: draft, in-review, scheduled, published, archived
  • Authors can save drafts without affecting the published version
  • Editors can schedule content for publish at a future exact datetime
  • Generate a preview URL that shows draft content to authorized users only
  • One-click rollback to any previously published version
  • Support multiple draft versions in parallel (e.g., seasonal variants)

Non-Functional Requirements

  • Scheduled publish must execute within 30 seconds of the specified time
  • Preview URL access must be unforgeable (signed token)
  • Publish operation must be atomic: readers never see a partial update

Data Model

The content_versions table stores all versions of every piece of content. Columns: version_id (UUID), content_id, version_number, status (draft/in_review/scheduled/published/archived), body (or S3 ref for large content), author_id, created_at, updated_at, scheduled_publish_at (nullable), published_at (nullable), metadata as JSON. Only one version per content_id may have status = published at any time; this is enforced with a partial unique index.

A published_content materialized table (or cache layer) stores the current published version per content_id for fast reads. This table is updated atomically when a publish event fires. Readers query this table; they never query content_versions directly.

The preview_tokens table stores: token_hash (SHA-256 of the signed token), version_id, created_by, expires_at, max_views (nullable). Tokens are signed with HMAC-SHA256 using a server secret, so validity can be verified without a database lookup, but revocation requires checking the table.

State Machine

Valid transitions are: draft to in_review, draft to scheduled, in_review to scheduled, in_review to draft (changes requested), scheduled to published (by the scheduler), scheduled to draft (unschedule), published to archived (deprecate), any state to draft (create new draft from existing). Transitions that are not listed are rejected. The state machine is implemented as a validation function called before every status update. Invalid transitions return a 422 error with a human-readable reason.

Core Algorithms

Scheduled Publish Execution

A scheduler process polls the content_versions table every 10 seconds for rows where status = scheduled AND scheduled_publish_at <= NOW(). For each candidate, it attempts to acquire a distributed lock (Redis SETNX with TTL) keyed on version_id. The lock prevents duplicate execution if multiple scheduler instances are running. The publish transaction: update the candidate version status to published, update the published_content table, set the old published version to archived, release the lock, and emit a content.published event. The entire database change runs in a single transaction for atomicity.

Preview Token Generation

Preview tokens encode the version_id, expiry timestamp, and a random nonce, signed with HMAC-SHA256. The token is base64url encoded and appended to a preview URL. On access, the server verifies the HMAC signature (no DB lookup needed), checks expiry, then checks the preview_tokens table to confirm the token has not been revoked and view count is within limit. If valid, the draft content is rendered and returned.

Rollback

Rollback creates a new version copying the body of the target historical version, sets status to published, and archives the current published version. This preserves full history; the rollback itself is a new version, not a mutation of old records. The rollback is atomic via a database transaction.

API Design

  • POST /content/{content_id}/versions — Create a new draft version
  • PUT /content/{content_id}/versions/{version_id} — Update draft body or metadata
  • POST /content/{content_id}/versions/{version_id}/transition — Change status; body contains target_status and optional scheduled_publish_at
  • POST /content/{content_id}/versions/{version_id}/preview — Generate a signed preview URL with configurable TTL
  • POST /content/{content_id}/rollback — Roll back to a specified version_id
  • GET /content/{content_id} — Return current published version (public endpoint, cached)

Scalability

Published content reads are served from the published_content cache (Redis or a CDN-backed edge cache). The cache key is content_id. On publish or rollback, the cache entry is invalidated synchronously within the publish transaction before committing. This ensures readers see the new version immediately after the transaction commits.

The scheduler is a single active instance with a warm standby (leader election via Redis or Zookeeper). It processes up to 1,000 scheduled publishes per minute, which is sufficient for typical CMS workloads. For higher throughput, the scheduler can be sharded by content_id range.

Interview Talking Points

Highlight the importance of atomic publish (transaction + cache invalidation in one step), the distributed lock on the scheduler to prevent duplicate publishes, and why rollback creates a new version rather than mutating old records. Interviewers often ask about the preview URL security model — explain why HMAC signing allows stateless verification while the DB check handles revocation.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you model a content state machine for a draft/review/scheduled/published workflow?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Define states (DRAFT, IN_REVIEW, SCHEDULED, PUBLISHED, ARCHIVED) and allowed transitions (e.g., DRAFT→IN_REVIEW, IN_REVIEW→SCHEDULED, SCHEDULED→PUBLISHED). Persist the current state on the content row and record each transition in an audit log table with (content_id, from_state, to_state, actor_id, timestamp). Enforce transitions in a domain service layer so no code path can skip states, and emit events on each transition for downstream consumers.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement scheduled publishing with precise timing?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store the scheduled_at timestamp in UTC on the content row when it enters SCHEDULED state. A scheduler process (cron or a dedicated job queue like Sidekiq or Celery) polls for rows where state = SCHEDULED AND scheduled_at <= NOW() and transitions them to PUBLISHED. For sub-minute precision, use a delayed job queue: enqueue a publish job with an execute_at time and let the queue worker fire it. Idempotency keys prevent duplicate publishes on retry."
}
},
{
"@type": "Question",
"name": "How do you generate and secure preview URLs for unpublished content?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Generate a signed, time-limited token (HMAC-SHA256 over content_id + expiry + secret) and append it as a query parameter to the preview URL. The preview endpoint validates the token signature and expiry before rendering the draft version. Store no server-side state — the token is self-validating. Set a short expiry (hours) and provide a way for authors to regenerate tokens, invalidating old ones by rotating the secret or using a per-content nonce."
}
},
{
"@type": "Question",
"name": "How do you implement rollback for published content?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Rollback re-publishes a previous version by creating a new revision whose content matches the target historical version and immediately transitioning it to PUBLISHED. The current live content is demoted to ARCHIVED (not deleted). This makes rollback a forward operation — the history chain stays intact and the rollback action is itself audited. CDN caches are purged via a cache invalidation call keyed to the content's URL after the new version is committed."
}
}
]
}

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Atlassian Interview Guide

See also: Twitter/X Interview Guide 2026: Timeline Algorithms, Real-Time Search, and Content at Scale

Scroll to Top