The architectural building blocks
Most integration architectures are composed from five building blocks:Pattern 1: one-way push (event-driven)
The default pattern for partner integrations that capture data on the source platform and synchronize it into Virtuous.When to use
- The source platform supports webhooks for the events you care about.
- The customer wants near-real-time data in Virtuous.
- The integration is a payment processor, fundraising platform, ESP, or similar event-producing source.
Implementations
Stripe to Virtuous, Mailchimp to Virtuous, Fundraising Platform to Virtuous, Auction/Event Platform to Virtuous. The foundational reference is Sync External Donations into Virtuous.Pattern 2: one-way push (polled)
The same destination, different source-event model. Used when the source platform doesn’t support webhooks or supports only a partial event set.When to use
- The source doesn’t support webhooks.
- The source has webhooks but for a tier the customer doesn’t subscribe to.
- Customer’s freshness requirement is tolerant (hourly or daily acceptable).
Implementations
Build a Nightly Data Sync is the canonical pattern.The hybrid variant
Constant Contact to Virtuous is a hybrid — webhooks for the events available, polling for the rest. The same outbound queue receives from both capture paths.Pattern 3: one-way pull (Virtuous → partner)
The reverse direction: a partner platform pulls data from Virtuous for its own consumption.When to use
- The partner platform is a reporting tool, BI integration, or data warehouse.
- The customer’s data flows from Virtuous outward (Virtuous is the source of truth).
- A reverse-direction integration of a Pattern 1 architecture.
Implementations
The inbound side of Build a Two-Way Sync instantiates this pattern. The reconciler component pattern comes from Reconcile Failed Syncs.Pattern 4: two-way sync
The composition of Pattern 1 and Pattern 3 — bidirectional sync between two systems.When to use
- The customer maintains data in both systems and expects them to stay consistent.
- Changes on either side should propagate to the other.
- The customer needs a “single view” across the two platforms.
Implementations
Build a Two-Way Sync is the canonical pattern. The two-way Mailchimp and Constant Contact recipes are specific instances.The hardest problem
Two-way sync introduces the sync loop: a write on one side triggers a webhook that triggers a write on the other side that triggers a webhook back, ad infinitum. The defense is source identification + per-record sync state — see Build a Two-Way Sync: The hardest problem.Pattern 5: bulk load + steady-state
The composition of a bulk-load pattern (for initial data backfill) with an ongoing sync pattern (for steady-state operation). Both phases share the same architecture.When to use
- A new customer is onboarding with existing historical data.
- The integration needs to import “everything before today” and then start syncing “everything after today.”
Implementations
Import Historical Gifts is the canonical bulk loader, with explicit guidance on how it composes with the steady-state architecture from Sync External Donations.Sharing the queue
The architectural choice that makes this pattern work: share the queue between bulk and steady-state. The submitter doesn’t distinguish; it drains everything. This means:- The same idempotency logic protects both bulk and steady-state submissions.
- The same throttling keeps both within the rate limit.
- The same monitoring catches issues in both.
State management patterns
Every sync architecture needs state. Three classes of state, each with its own storage pattern:Sync state per record
For each entity (Contact, Gift, RecurringGift) you sync, persist:- Idempotency (don’t re-submit something already confirmed).
- Reconciliation (find records in inconsistent states).
- Audit (what state was this record in at time T).
Checkpoint per integration
For incremental sync, track the high-water mark of what’s been processed:Dead-letter store
For records that exhausted retries — see Error Recovery Patterns for the schema and pattern.Multi-tenant isolation
Partner integrations serving multiple customers need isolation along several axes:customer_id, and run per-customer worker instances (or per-customer scheduled cron slots). This produces clear blast-radius isolation: any failure in one customer’s sync is contained to that customer.
For very small partner integrations (10–20 customers), a single multi-tenant worker that processes one customer at a time is acceptable. For larger scale, per-customer dedicated workers are simpler to operate.
Initial reconciliation patterns
When an integration first connects to a customer, neither side has knowledge of the other’s records. The initial reconciliation establishes mutual awareness.Pattern A: full bulk-load (one-direction)
For Pattern 1 (one-way push), the initial reconciliation is just the bulk-load pattern — pull every relevant source-side record, queue it, let the submitter process. See Import Historical Gifts.Pattern B: cross-matching reconciliation (two-way)
For Pattern 4 (two-way sync), more nuanced. Records may already exist on both sides:Pull all relevant records from both sides
POST /api/Contact/Query (with the right filter) to enumerate Virtuous-side. Enumerate partner-side from your platform’s database.Cross-match by stable identifiers
contactReferences[] for your platform’s source. For each partner-side record, check for an existing Virtuous Contact ID. Build the matched pairs.Resolve unmatched records
Establish baseline state
in_sync in the sync state table. Future events flow through the steady-state architecture.Evolving between patterns
Most integrations start with one pattern and need to evolve to another as requirements change. Three common evolutions:Polled → event-driven
If the source platform adds webhook support, migrating from Pattern 2 to Pattern 1 is straightforward because the queue and submitter are unchanged:Add the webhook receiver
Validate parity
Reduce polling cadence
Retire if appropriate
One-way → two-way
Adding inbound sync to a one-way push integration requires careful attention to sync loops:Subscribe to Virtuous webhooks
contactCreate/contactUpdate/giftCreate event subscriptions.Add per-record sync state
Implement source identification
transactionSource / referenceSource so the inbound handler can distinguish “from us” vs. “from elsewhere.”Roll out inbound processing
Add full inbound logic
Single-tenant → multi-tenant
A POC integration for one customer often becomes a multi-customer offering. The migration:Add the customer_id column everywhere
Refactor workers to be customer-aware
Move credentials to a secrets manager
Onboard the second customer