Skip to main content
← All docs
FFlowcore
    flowcore

    Design event-driven applications on Flowcore

    Bergur Davidsen·Updated 2026-07-16

    |Open in||History

    An event-driven Flowcore application records durable facts first and derives application state from them. The event log explains what happened; databases, search indexes, and caches provide the views needed to use that history efficiently.

    You do not need to convert every service into a full event-sourcing implementation at once. Start at a business boundary where durable history, independent consumers, or replay already provides clear value.

    Model facts instead of database operations

    A useful event describes a completed business outcome: an order was placed, a payment was authorized, or a document was approved. It should still make sense to a developer who does not know the producer's database schema.

    Names such as record.updated or database.changed hide meaning and force consumers to understand implementation details. Prefer a stable domain name such as order.cancelled.0, with a payload that contains the information needed to understand that fact.

    Commands and events are different. CancelOrder asks the system to perform an action. order.cancelled.0 states that the action was accepted and happened. Validate permissions and business rules before publishing the event.

    Choose durable domain boundaries

    A data core should represent a coherent ownership and governance boundary. It often aligns with a product domain, a team that owns the vocabulary, or data that shares retention and access requirements.

    Flow types group related lifecycles within that boundary, while event types identify individual facts. For example, a commerce data core might contain an order.0 flow type with order.placed.0, order.paid.0, and order.cancelled.0 events.

    Do not create one data core per microservice merely because deployment boundaries differ. That fragments history and complicates permissions, replay, and discovery. Split data cores when ownership or governance differs, not whenever code moves to another repository.

    Keep the command path small

    A typical write path authenticates the caller, validates the command, checks business rules, and publishes the accepted outcome. The ingestion response confirms that Flowcore stored the event; it does not confirm that every downstream consumer has finished.

    async function placeOrder(input: PlaceOrderInput) {
      await authorizeOrder(input.customerId)
      const order = validateAndPriceOrder(input)
     
      return pathways.write("order.0/order.placed.0", {
        data: {
          orderId: order.id,
          customerId: order.customerId,
          totalMinor: order.totalMinor,
          currency: order.currency,
          placedAt: new Date().toISOString(),
        },
        metadata: {
          correlationId: input.requestId,
          source: "checkout-api",
        },
      })
    }

    The command handler should not update several downstream databases synchronously to make the event appear complete. Publish the durable fact, then let independently operated consumers build the state they own.

    Use metadata for traceability

    Correlation and causation metadata make cross-service workflows understandable. A correlation ID connects events created during one business process. A causation ID identifies the event or command that triggered the next action.

    Keep required business data in the payload. Metadata is not a hidden extension of the contract, and it must not contain API keys, bearer tokens, or unrestricted personal data.

    When a workflow has meaningful failure or compensation behavior, represent that behavior explicitly. An event such as payment.declined.0 tells a clearer operational story than an error that exists only in one consumer's logs.

    Design contracts for independent consumers

    Every event type needs a runtime-validated contract. TypeScript types help at compile time, while Zod or another runtime schema protects the boundary where untrusted or historical data enters the handler.

    Add optional fields when the meaning remains compatible. When required fields, semantics, or structure change incompatibly, publish a new event-type version. Existing history must retain its original meaning, and consumers need time to adopt the new version deliberately.

    A shared contract package is useful when several services use the same event paths. Keep it scoped to a domain rather than collecting unrelated contracts into one package that forces every service to upgrade together.

    Choose how consumers react

    The simplest consumer updates a local projection. Other consumers may send notifications, populate analytics, or initiate another business process. Each consumer should own its checkpoint, idempotency, and failure handling.

    Avoid a single large handler that combines a database update, email, partner API call, search indexing, and analytics. One failure then retries every side effect together. Separate consumers or publish follow-up events when the work has different ownership or reliability needs.

    Request-response behavior can still exist. An API may publish an event and wait briefly for a local projection, but the timeout must be explicit. A timed-out client cannot assume ingestion failed, and the service must handle a retry with a stable command identity.

    Treat projections as replaceable

    A projection is a query model derived from events. It may be a PostgreSQL table, search index, cache, warehouse model, or vector index. Its schema should match the questions it serves rather than mirror the event payload mechanically.

    Store source event identity in important projection rows and make handlers idempotent. When projection logic changes, rebuild the projection from retained history instead of rewriting events.

    This distinction is central to Flowcore architecture: the event log is durable business history, while a projection is an optimized interpretation that can be corrected or replaced.

    Adopt Flowcore incrementally

    A practical first step is to publish one high-value fact and build one projection from it. Once the team can observe delivery, handle duplicates, and replay safely, add consumers or move another workflow onto the event model.

    Split services when ownership or scaling requires it, not because event-driven architecture demands many processes. A well-structured modular service can publish and consume Flowcore events without becoming a distributed system prematurely.

    Continue with Integrate external systems and downstream consumers for adapters, transform endpoints, and external side effects.

    PreviousRun operational readiness and retention workflowsNextIntegrate external systems and downstream consumers