To understand how different architectural patterns compare in terms of these quality attributes, see Architectural Patterns Comparison.
Active migration. The legacy Next.js app (
apps/web, port 7803) is being replaced by apps/tanstack-web (TanStack Start) plus apps/backend (Rust, port 7820). See TanStack Start And Rust Migration. The quality attributes below survive the migration; only the runtimes hosting them change.Extensibility (5 Reasons)
The event-driven architecture is fundamentally designed for evolution and the seamless addition of new functionality.1. The “Add a Consumer” Pattern (Open/Closed Principle)
To introduce new business functionality, you add a new handler that reacts to existing work rather than editing the existing code path. In Tuturuuu that handler is a new Trigger.dev task (and, conceptually in a broker-based system, a new consumer subscribed to an event stream). Example: To add a fraud-detection capability, we can introduce a new Trigger.dev task that runs whenever a payment is attempted or fails, without modifying the code that initiates payments. In Trigger.dev v4, a task is a plaintask() export; an upstream handler triggers it by name (yourTask.trigger(payload) / tasks.trigger(...)).
fraudDetection.trigger(...) call (or none, if a shared dispatcher already fans out payment events). The existing charge logic is untouched.
Benefits:
- Zero modification to the payment logic
- Independent deployment of the fraud-detection task
- No regression risk to existing functionality
- Team autonomy - the fraud team works independently
2. Introduction of New, Non-Breaking Events
When a new data source is introduced, like a new type of IoT sensor, we can introduce new event types (e.g.,SensorReadingRecorded). Existing services will simply ignore these new events, ensuring they are not impacted. New, specialized services can then be built to handle this new stream, allowing the system to grow organically.
Example: in a Trigger.dev v4 model, a “new event type” becomes a new task plus the call site that triggers it. Existing tasks never see the new payload, so they are unaffected.
- Non-breaking changes to system
- Gradual adoption of new features
- Backward compatibility maintained
- Experimentation with low risk
3. Compatible Schema Evolution
The architecture utilizes schema validation (Zod) to govern event structures. This allows us to evolve event payloads in a compatible manner, such as adding a new, optionalcorrelationId field to an existing event without breaking any older consumer services that are not yet aware of the new field.
Example:
- Gradual migration of consumers
- No breaking changes for existing services
- Type safety with Zod validation
- Clear documentation of event structure
4. Modular Data Ownership
Conceptual pattern. In a full microservice mesh each service owns a private database. Tuturuuu does not do this today - all apps share a single Supabase Postgres project, and per-feature isolation is enforced through schema boundaries, table ownership conventions, and RLS rather than separate physical databases. The pattern below shows how decoupled ownership would look and is useful when reasoning about future extraction; it is not the current deployment.To support a new feature requiring geospatial queries, you can isolate the new data shape behind a dedicated module (its own tables, or its own database extension such as PostGIS) so it evolves independently of unrelated tables.
- Right tool for the job - choose the optimal storage for each feature
- Independent evolution of data models
- Bounded blast radius for schema changes
- Module isolation - feature data evolves without cross-feature migrations
5. Frontend Composability
The modular frontend can be extended with new components that drive new interactions. A new dashboard widget can call a REST route (/api/v1/... in apps/web, or a Rust apps/backend endpoint in the migrated stack), and that route either responds directly or kicks off background work by triggering a Trigger.dev task. The widget never talks to a broker; it talks to an HTTP endpoint that owns the side effects.
Example:
- Frontend-driven innovation - the UI team can add features
- Clean integration via stable HTTP routes that own the side effects
- Backend extensibility without tight coupling to the client
- Feature experimentation with low risk
apps/web to apps/tanstack-web + apps/backend migration.
Resilience (5 Reasons)
Resilience is an intrinsic property of this loosely coupled, asynchronous architecture.1. The Queue as a Stability Buffer (Temporal Decoupling)
If downstream processing is slow or temporarily unavailable, the code that triggers it is unaffected: triggering a Trigger.dev task enqueues a run and returns immediately. Trigger.dev persists and retries runs, so work is not lost while a task’s worker capacity catches up. The same buffering property holds in a full broker too; in Tuturuuu it is provided by Trigger.dev’s managed run queue rather than a self-hosted broker. Example:- Zero data loss during outages
- Automatic recovery when service restarts
- Producer isolation from consumer failures
- System resilience to partial failures
2. Asynchronous, Non-Blocking Communication
Producers “fire and forget” events without waiting for a response. This prevents cascading failures, where a slow consumer would otherwise block an upstream service and cause a system-wide slowdown. Example:- Fast user responses - no blocking on background work
- Isolation from slow consumers
- Better resource utilization
- Improved user experience
3. Idempotent Consumer Design
A core resilience pattern is to make task handlers idempotent so they can safely run on the same payload more than once without duplicate side effects. Trigger.dev retries failed runs (and supports anidempotencyKey on trigger), so a task that crashes after sending an email but before recording it may run again; idempotency ensures that retry does not corrupt state.
Example:
- Safe retries without side effects
- Data consistency even with failures
- Simple error recovery - just retry
- Reliable processing guarantees
4. Retries and a Dead-Letter Path for Error Handling
Conceptual + partial. A true dead-letter queue (a separate topic the broker auto-routes poison messages to) is a broker primitive Tuturuuu does not run. Trigger.dev v4 does provide the durable building blocks: per-taskFor payloads that consistently fail (e.g., malformed data), configure bounded retries and route the still-failing payload to a dedicated task instead of letting it block the rest of the workload.retrypolicies and the ability to hand a permanently failing payload off to a dedicated “dead-letter” follow-up task for offline analysis. The example below uses real v4retryconfig and models the dead-letter step as an explicit follow-up trigger.
Confirm the exact
retry / handleError field names against the installed @trigger.dev/sdk v4 types before copying this verbatim; the shape above illustrates the pattern, and Tuturuuu’s own tasks in packages/trigger/src favor catching errors inside run and returning a { success: false } result.- Poison pill isolation - one bad message doesn’t stop queue
- Automatic retry for transient errors
- Manual review for persistent errors
- Pattern detection for systemic issues
5. Replayability for Disaster Recovery
Conceptual - not implemented in Tuturuuu. This is a classic event-sourcing property: when an immutable, ordered log of domain events is the source of truth, you can rebuild any derived state by replaying that log from a known-good point. Tuturuuu is not event-sourced - state lives in Supabase Postgres tables, recovery relies on Postgres backups/PITR, and Trigger.dev v4 has no “replay this date range of events” trigger like the one previous versions of this page implied. Keep this section as a mental model for derived/cached state, not as an operational runbook.How it would work (event-sourcing concept):
- State reconstruction from a durable event log
- Bug-fix validation by re-deriving projections
- Audit trail for compliance
Scalability (5 Reasons)
The event-driven model is inherently designed for high-throughput and elastic scaling.1. Parallel Processing via Concurrency
To increase throughput, you raise the task’s allowed concurrency and let more runs execute in parallel. In a Kafka-style system this is done with consumer groups; in Tuturuuu it is Trigger.dev’s managed concurrency - you define the task once and set aqueue.concurrencyLimit (and queue-level controls) rather than running and balancing your own consumer instances.
Example:
- Throughput scales with concurrency - raise the limit, process more in parallel
- Managed scheduling - no consumer instances to run or balance yourself
- No code changes required to scale
- Cost-effective - tune concurrency up or down as needed
2. Per-Entity Ordering with Parallelism Across Entities
A common scaling goal is: process work for a single entity (e.g. one workspace) in order, while processing different entities fully in parallel. Kafka achieves this with topic partitions keyed by a business key; Tuturuuu does not run Kafka topics. The closest Trigger.dev mechanism is a concurrency key plus per-queue concurrency limits, which serialize runs that share a key while letting different keys run in parallel. Example:This maps the partitioning concept onto Trigger.dev v4. Verify the
concurrencyKey option name against the installed SDK before relying on it; there are no Kafka topics, partitions, or broker ACLs in Tuturuuu.- Ordering guarantees per entity (per concurrency key)
- Maximum parallelism across distinct entities
- Optimal throughput with consistency
- Scalable architecture
3. Independent Scaling per Workload
Different workloads scale on their own axis. In Tuturuuu this shows up two ways: the deployable apps (apps/web, the migration target apps/tanstack-web, and the Rust apps/backend) scale at the deployment layer, and each Trigger.dev task carries its own concurrency budget so a heavy task can run wide while a light one stays narrow - without coupling either to a single shared scale knob.
Example:
- Granular scaling per service
- Cost optimization - only scale what’s needed
- Resource efficiency
- Performance optimization per workload
4. The Run Queue as a Load Absorber
A managed run queue can absorb sudden bursts of triggered work. It smooths out load so tasks process at their own sustainable pace (bounded by concurrency) without being overwhelmed. In Tuturuuu this absorber is the Trigger.dev run queue; the same role would be played by a broker in a Kafka-style system. Example: Benefits:- Spike protection - absorbs bursts
- Sustainable processing rates
- No service overload
- Improved reliability
5. CQRS for Optimized Read Performance
An asynchronous, task-based flow is a natural fit for Command Query Responsibility Segregation (CQRS). A “command” writes the authoritative record and then triggers a background task; that task maintains a read-optimized model (for example a denormalized cache), letting read-heavy paths scale independently and deliver low latency.
Conceptual. Tuturuuu does not run a formal CQRS split today; this shows how the pattern maps onto the real task() API if you adopt it for a read-heavy surface.
Example:
- Read/write optimization separately
- Low-latency reads from cache
- Consistent writes to database
- Independent scaling of read vs write paths
Summary Matrix
Related Documentation
- Event-Driven Architecture - Detailed advantages and drawbacks
- Architectural Decisions - Why we chose this architecture
- Trigger.dev Package - Implementation reference