惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
博客园_首页
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
D
DataBreaches.Net
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
D
Docker
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
量子位
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
The Blog of Author Tim Ferriss
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
Cloudbric
Cloudbric
博客园 - 司徒正美
H
Help Net Security
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LangChain Blog
Latest news
Latest news
M
MIT News - Artificial intelligence
T
Threat Research - Cisco Blogs
博客园 - Franky
S
Security Affairs
W
WeLiveSecurity
F
Full Disclosure
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
Cyberwarzone
Cyberwarzone
美团技术团队
PCI Perspectives
PCI Perspectives
C
Check Point Blog
Spread Privacy
Spread Privacy

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Just Shipped FilmTailor's Scheduling Feature and Chose Event Sourcing. Sorry Not Sorry
Andy Robinson · 2026-06-17 · via DEV Community

The history of a film schedule matters as much as its current state. That one insight changed everything about how we built this.


At FilmTailor, we shipped our scheduling feature this week. It lets film productions manage their shoot schedules: strips, cast assignments, scene ordering, the works. It sounds straightforward. It is absolutely not straightforward.

This post covers the architectural choices we made, why we made them, and the honest trade-offs involved. If you've ever been tempted by event sourcing but weren't sure whether it was worth the overhead, hopefully this helps.


What Does the Scheduling Service Actually Do?

Before we talk architecture, a quick primer on the domain.

A "schedule" in film production is a breakdown of the shoot: which scenes get filmed on which days, who's required on set, what equipment is needed. The core unit is a "strip" (think of a physical index card in the old system, representing a single scene). Strips get ordered, reordered, archived, imported from scripts, and annotated with production details.

The key insight that shaped our architecture: schedules change constantly, and the history of those changes matters.

A production coordinator needs to know who reordered the strips on Tuesday afternoon, because that coincided with a cast member being added and now the schedule is wrong. "Just check the database" doesn't help you if you've already overwritten the old order.

That insight led us straight to event sourcing.


Why Event Sourcing? (And Why Not Just CRUD?)

Let's be honest: CRUD over a MediatR pipeline is the path of least resistance in .NET. It's what most developers reach for, and for most problems, it's the right call. You get:

  • A simple mental model (rows go in, rows come out)
  • Straightforward querying
  • Libraries and patterns the whole team already knows

So we had to genuinely justify not doing that.

The case for CRUD

Here's what a standard approach would have looked like:

POST /schedules/{id}/strips/reorder
→ Update strip positions in the database
→ Done

Quick to implement, easy to reason about, well-understood performance characteristics. The database is the state.

Where CRUD falls down for scheduling

The problem is that a schedule isn't just a snapshot, it's a process. Productions run for weeks or months. A schedule might be reordered dozens of times before a shoot day. Questions like "what did this schedule look like three days ago?" or "which user archived this strip?" are not edge cases. They're core to the product.

With CRUD, answering those questions means bolting on an audit log after the fact. You end up with two sources of truth (your main table and your audit table), and they inevitably drift.

With event sourcing, the audit log is the source of truth. Every change is an immutable, timestamped event. The current state is simply what you get when you replay those events.

ScheduleCreated { by: "user-123", at: "2024-06-01T09:00:00Z" }
StripAdded      { stripId: "s-001", position: 0, by: "user-123" }
StripAdded      { stripId: "s-002", position: 1, by: "user-456" }
StripReordered  { stripId: "s-001", newPosition: 1, by: "user-456" }

You can ask "what was the order at 10am?" and get a real answer. You can see who made what change and when. No separate audit infrastructure required.

An underrated benefit: projecting the same events into multiple read models

Here's something that doesn't get talked about enough. Because your events are the ground truth, you're not locked into a single representation of your data. You can project the same event stream into as many materialised views as you like, each shaped for a different purpose.

We currently maintain a ScheduleBoardView (optimised for rendering the drag-and-drop board UI) and a StripFieldSuggestionsView (optimised for autocomplete). Both are built from the same underlying event stream. Adding a third for reporting, or a fourth for a future mobile client with a different data shape, costs almost nothing architecturally.

Even better: if you need to change a read model's structure (say, you want to add a computed field, or you realised the data shape is wrong), you just update the projection code and tell Marten to rebuild. It replays all the events from the beginning and reconstructs the view from scratch. This would be a painful migration script in a CRUD system. With event sourcing, it's a configuration option.

// Marten will rebuild this projection from scratch on next startup
options.Projections.Add<ScheduleBoardProjection>(ProjectionLifecycle.Inline,
    asyncConfiguration: x => x.EnableDocumentTrackingForQuery = true);

This was one of those "oh, this is actually great" moments during development. We changed the shape of ScheduleBoardView twice during the build. Each time, it was a non-event (pun intended).


The Stack: Marten + PostgreSQL

We chose Marten as our event store, running on PostgreSQL.

There are purpose-built event store options out there. The most prominent is Kurrent (formerly EventStoreDB), which is mature, battle-tested, and has excellent tooling - I've personally used it for a project at NHS Wales, with great success. We ruled it out at this stage for a straightforward reason: operational complexity. Kurrent is a separate database engine with its own deployment, monitoring, backup strategy, and failure modes. For a team at our current scale, adding that to the stack felt premature.

That said, it's genuinely on our radar for the future. One of the underappreciated benefits of building on event sourcing from the start is that the events themselves are portable. If we ever outgrow Marten or want Kurrent's more advanced features (persistent subscriptions, projections across multiple streams, that sort of thing), migrating the event log across is fairly mechanical. The events are just JSON records with timestamps. We're not locked in.

For now, Marten gives us most of what we need while staying on infrastructure we already understand.

It's PostgreSQL all the way down. We already use Postgres. Marten stores events as JSONB and gives you a proper event stream abstraction on top. No separate event store to run, monitor, or secure.

Projections are first-class. Marten has a mature projection system that keeps read models up to date automatically. The projection code is just C# — no separate query language to learn.

It's been around long enough to be boring. Boring is good in production infrastructure. Marten is well-maintained, has excellent documentation, and the community is active.

Here's roughly what our aggregate looks like:

public class ScheduleAggregate
{
    public Guid Id { get; private set; }
    public ScheduleStatus Status { get; private set; }
    public List<StripState> Strips { get; private set; } = [];

    // Command handler: validates, then produces events
    public IEnumerable<object> Handle(ReorderStrips command)
    {
        if (Status != ScheduleStatus.Draft)
            throw new ScheduleDomainException("Only draft schedules can be reordered");

        // Validation logic here...

        yield return new StripsReordered(command.NewOrder, command.UserId, DateTimeOffset.UtcNow);
    }

    // Apply method: pure state reconstruction from event
    public void Apply(StripsReordered @event)
    {
        // Update in-memory state based on event
        // No side effects, no DB calls, just state mutation
    }
}

The Handle method validates the command and emits events. The Apply method reconstructs state from events. These two concerns stay completely separate, which makes the business logic surprisingly easy to unit test.


CQRS: Splitting Reads and Writes

Event sourcing pairs naturally with CQRS (Command Query Responsibility Segregation), because once you've committed to storing events rather than state, you need a read model strategy.

Replaying a full event stream every time someone loads a schedule board would be daft. Instead, we use Marten's projection system to maintain a ScheduleBoardView: a pre-computed, read-optimised snapshot that gets updated whenever new events are appended.

public class ScheduleBoardProjection : SingleStreamProjection<ScheduleBoardView>
{
    public void Apply(ScheduleBoardView view, StripAdded @event)
    {
        view.Strips.Add(new StripSummary
        {
            Id = @event.StripId,
            Position = @event.Position,
            // ... other fields
        });
    }

    public void Apply(ScheduleBoardView view, StripsReordered @event)
    {
        // Update positions in the read model
    }
}

We configure these as inline projections, meaning they run synchronously as part of the same transaction that appends the events. The read model is always up to date before the HTTP response goes out. No eventual consistency lag for the client to deal with.

This was a deliberate choice. Async projections are more scalable under extreme load, but they introduce a window where the read model lags behind. Given the collaborative, real-time nature of schedule editing, we didn't want users to submit a change and immediately get a stale read back.

The trade-off: synchronous projection adds a small amount of latency to write operations. We've found this completely acceptable at current scale, and we're monitoring it.


Real-Time Updates with SignalR

Schedules in film production are collaborative. Multiple people might be looking at the same schedule simultaneously. We didn't want someone's reorder to be invisible to everyone else until they refreshed.

We added a SignalR hub that broadcasts to connected clients whenever the schedule changes. The flow is simple:

  1. Command comes in, events are appended, projection is updated
  2. After the write completes, the hub notifies all clients subscribed to that schedule
  3. Clients re-fetch the schedule board (or we push the delta — we're evolving this)

In production, this runs through Azure SignalR Service rather than in-process, which means it scales horizontally without us having to worry about sticky sessions or shared state across container instances.


Honest Reflections: What's Hard About Event Sourcing

We'd be doing you a disservice if we made this sound like pure upside. Here's what we'd warn you about.

The learning curve is real. If your team is used to thinking in rows and tables, the mental shift to "what happened" rather than "what is" takes time. The aggregate pattern, the apply/handle split, the projection lifecycle: these are all new concepts to internalise.

Schema evolution requires care. Events are immutable, which means you can't just change an event's shape and re-run a migration. Marten has event versioning and upcasting support, which we're leaning on, but it's something you need to think about from the start rather than retrofitting.

Querying across aggregates is awkward. Event sourcing is brilliant for single-aggregate queries. Cross-aggregate reporting (for example, "show me all schedules across this organisation with more than 50 strips") requires projections designed for the purpose, or a separate read store. Our organisation-level metrics endpoint required a dedicated projection rather than a simple SQL query.

The event store grows over time. We're not at a scale where this is a problem, but for long-lived, high-activity aggregates, you'd want a snapshot strategy to avoid replaying thousands of events on load. Marten supports snapshots; we haven't needed them yet.


Would We Do It Again?

Yes, for this domain, unambiguously.

The audit trail alone has already paid for the added complexity. We've had two instances during development where a schedule ended up in an unexpected state, and being able to replay the exact sequence of events that led there made debugging trivially easy. With CRUD, we'd have been staring at the current state and guessing.

That said, event sourcing is a tool, not a philosophy. We have other services in the FilmTailor platform that are genuinely just CRUD: user preferences, notification settings, that sort of thing. Those don't need an event history. They use a standard EF Core + MediatR pattern and are simpler for it.

Reach for event sourcing when:

  • The history of changes matters to the business, not just the current state
  • You need auditability without bolting on a separate audit system
  • Your domain has rich, behavioural rules that benefit from an explicit aggregate model
  • You want flexibility to evolve your read models without painful data migrations

Reach for CRUD when:

  • You're managing configuration or reference data
  • The current state is all that matters
  • Speed of development is the priority and the domain is simple

Sorry Not Sorry

If you've read my previous post about Tailwind, you'll know I'm not shy about having opinions on technical choices. This one follows the same spirit.

Are we confident in these architectural decisions? Yes. Did we make them carelessly? No. We looked at the trade-offs, understood what we were taking on, and made a deliberate call based on the domain in front of us. That's not the same as being reckless, it's just being an engineer.

But here's the thing: confident doesn't mean infallible. Event sourcing might bite us in ways we haven't anticipated yet. The projection rebuild story might get complicated at scale. Kurrent might start looking very attractive once the event volume grows. Schema evolution might turn out to be more painful than we've planned for. To be honest, that would be a wonderful problem to have - it'll mean that we have users using the product we've been building for some time.

If any of that happens, we won't be sorry for having made these choices. We'll be glad we made them deliberately, understood why they seemed right at the time, and are therefore in a much better position to understand why they stopped working. That's not failure. That's how a team gets better.

The only architectural decisions worth being sorry for are the ones made without thinking. These weren't.


The scheduling service is live as of this week. If you're building tools for film and TV production and want to chat architecture, or just want to compare notes on Marten in production, find us at filmtailor.ai.

The FilmTailor Engineering Team

Check out how we're building the future of film production