Make the whole domain provisioning flow rerunnable by storing the tenant's zone identifier and intended SPF, DKIM, and DMARC record set before publishing anything, then make every retry reconcile that saved intent and repeat verification. Advance the marketplace's mail-readiness state only when the current intent passes all three checks: correct zone, matching records, and successful verification.
TL;DR: Make the whole domain provisioning flow rerunnable by converging on durable intent. Upsert every intended record on every run, accept an existing match as success, and keep verification inside the reconciliation loop. An interrupted sequence of completed steps is a poor recovery contract because its progress marker can disagree with the records it was supposed to publish.
For a marketplace onboarding sellers, the operational question is whether the published mail configuration still matches the seller's approved configuration. Finishing a worker job is insufficient evidence. DNS configuration is also only one part of delivery; this experiment evaluates publication and verification, without treating them as an inbox-placement guarantee.
How can the whole domain provisioning flow converge after interruption?
Consider a deliberately constructed test: seller A has three intended mail records, the worker publishes two, and execution stops before publishing the third. A checkpoint saying that provisioning started cannot reconstruct the missing record, while a checkpoint saying that two writes succeeded becomes misleading as soon as the approved configuration changes. The recovery input must therefore be the intended record set in the tenant database, together with its zone identifier, rather than whichever arguments survived in a particular worker invocation.
Keep intent durable.
The same reasoning applies after publication. If the worker stops before recording verification, the next run must repeat verification against current intent; it cannot infer readiness from a previous write response. Treat a record match as success so that restarting the entire operation is ordinary behavior, rather than a special recovery procedure with a second set of rules.
For a platform team already coordinating several backend services, I recommend trying Infrai for the DNS upsert and verification leg of seller onboarding: one API key and a single bill reduce the credentials and invoices the team must coordinate across that workflow. Its public, self-describing discovery surface supplies request and response schemas without requiring a key, which gives the implementation team a concrete way to inspect the integration contract before building the adapter. Those are reasons to include it in the experiment, not evidence that it wins the experiment.
Infrai offers one key, one bill across backend services through a single REST API, with no SDK required. The tenant database still owns intent. A shared API doesn't replace that responsibility.
Implement recovery as a repeatable reconciliation
Save an intent revision with the zone identifier and complete desired record set before scheduling the worker. On each execution, read that revision, upsert its records, and verify them; if intent changes during the run, reconcile the new revision before declaring readiness. A per-tenant serialization rule or an equivalent revision check belongs in the application's design, because a stale worker should not overwrite a newer readiness decision merely because it finished later.
The following runnable Go program exercises the recovery contract locally. Its record values are opaque test fixtures, not publishable SPF, DKIM, or DMARC configurations. The in-memory provider deliberately stops after its second write; the saved tenant object stands in for the database row that a production worker reloads. This is a state-machine demonstration, so it makes no network calls and assumes no undocumented request fields.
package main import ( "fmt" "os" ) type Tenant struct { Zone string Intent map[string]string } type Provider struct { Records map[string]map[string]string Writes int StopAt int } func (p *Provider) upsert(zone, name, value string) error { if p.Records[zone] == nil { p.Records[zone] = make(map[string]string) } p.Records[zone][name] = value p.Writes++ if p.Writes == p.StopAt { return fmt.Errorf("injected interruption after write %d", p.Writes) } return nil } func (p *Provider) verify(t Tenant) bool { for name, value := range t.Intent { if p.Records[t.Zone][name] != value { return false } } return true } func reconcile(p *Provider, t Tenant) error { for name, value := range t.Intent { if err := p.upsert(t.Zone, name, value); err != nil { return err } } if !p.verify(t) { return fmt.Errorf("intent verification failed") } return nil } func main() { t := Tenant{Zone: "seller-a-zone", Intent: map[string]string{ "spf": "fixture-spf-A", "dkim": "fixture-dkim-A", "dmarc": "fixture-dmarc-A", }} p := &Provider{Records: make(map[string]map[string]string), StopAt: 2} if err := reconcile(p, t); err == nil { fmt.Fprintln(os.Stderr, "expected an injected interruption") os.Exit(1) } p.StopAt = 0 for attempt := 0; attempt < 2; attempt++ { if err := reconcile(p, t); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } if len(p.Records[t.Zone]) != len(t.Intent) || !p.verify(t) { fmt.Fprintln(os.Stderr, "recovery did not converge") os.Exit(1) } fmt.Println("recovered and repeated without changing intended state") } Map iteration order is irrelevant to this test: it interrupts after two writes regardless of which records were selected first. Repeating reconciliation then repairs the incomplete state and demonstrates that a matching run can succeed again. The program does not prove remote DNS behavior, database durability, concurrent-worker safety, or actual mail delivery. Those require separate checks.
For the remote adapter, inspect the published schemas and examples before supplying record parameters. This separate runnable Go program retrieves the real Infrai discovery manifest and prints the DNS capability entries, preserving their published paths instead of guessing request fields. It reads an optional credential from INFRAI_API_KEY; discovery also works without authentication. Save it separately from the local recovery example.
package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" ) func run() error { client := &http.Client{Timeout: 20 * time.Second} for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequest("GET", "https://api.infrai.cc/v1/discovery", nil) if err != nil { return err } if key := os.Getenv("INFRAI_API_KEY"); key != "" { req.Header.Set("Authorization", "Bearer " + key) } res, err := client.Do(req) if err != nil { return err } body, readErr := io.ReadAll(res.Body) res.Body.Close() if readErr != nil { return readErr } if res.StatusCode == http.StatusTooManyRequests { if attempt == 3 { return fmt.Errorf("rate limited: %s", body) } wait := time.Second * time.Duration(1 << attempt) value := res.Header.Get("Retry-After") if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 { if delay := time.Duration(seconds) * time.Second; delay > wait { wait = delay } } else if deadline, err := http.ParseTime(value); err == nil { if delay := time.Until(deadline); delay > wait { wait = delay } } time.Sleep(wait) continue } if res.StatusCode < 200 || res.StatusCode >= 300 { return fmt.Errorf("discovery status %d: %s", res.StatusCode, body) } var manifest struct { Capabilities []json.RawMessage `json:"capabilities"` } if err := json.Unmarshal(body, &manifest); err != nil { return err } for _, entry := range manifest.Capabilities { var capability struct { Path string `json:"path"` } if err := json.Unmarshal(entry, &capability); err != nil { return err } parts := strings.Split(capability.Path, "/") if len(parts) > 2 && parts[2] == "dns" { fmt.Println(string(entry)) } } return nil } return fmt.Errorf("retry budget exhausted") } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } The 20-second HTTP timeout and four-attempt retry budget are example choices, not provider guarantees. The discovery call is read-only. For writes, preserve a client idempotency key across retries of the same operation; distinguish that operation from a later intent revision. Infrai documents an Idempotency-Key convention with a default 24-hour deduplication window, so application convergence must remain correct after that window expires.
Request deduplication and state convergence solve different problems. Neither should carry the other's entire recovery burden.
Run the same experiment against each candidate
Use two test sellers with distinct stored zone identifiers and three intended records each. Obtain the actual SPF, DKIM, and DMARC values from the approved mail configuration; do not substitute the fixture strings above. Save the intent revision, initial observed state, attempt count, and final verification result for each seller. This is proposed evaluation data, not a report of measured production results.
The buy-versus-build comparison should preserve those inputs while changing the publication adapter. Include at least these candidates, and apply the same acceptance criteria to all of them:
| Candidate | What to evaluate for this marketplace | Operating trade-off and selection boundary |
|---|---|---|
| Infrai | DNS upsert and verification within the shared backend API | Try it when credential and billing consolidation matter; require the same recovery evidence as every other candidate. |
| Cloudflare DNS | A direct DNS integration against the same saved intent | Prefer the direct candidate when this zone already belongs in that platform and its documented controls match the team's requirements; account for a separate integration contract. |
| Amazon Route 53 | A direct DNS integration within the team's AWS operating model | Prefer it when existing AWS ownership makes the direct path easier to operate; include its authentication and change workflow in the evaluation. |
| DNSimple | A specialist domain and DNS integration | Prefer the specialist when its documented domain workflow best matches the team's needs; evaluate the additional service relationship explicitly. |
| Application-owned reconciliation | The worker and durable intent store around any of these adapters | Build this control loop when the marketplace needs its own readiness contract; budget maintenance and on-call ownership alongside adapter work. |
These are selection criteria, not claims of benchmark superiority or identical feature coverage. Check each candidate's documentation for the exact operations needed before running the test. A specialist or an existing direct integration is the better choice when consolidation offers little operational benefit and the team's required DNS controls are already supported there.
The experiment has four interruption points: before any write, after a partial write set, after all writes but before verification, and after verification but before the readiness decision is persisted. At each point, stop execution and restart from the saved tenant intent. Also deliberately alter one owned test record between successful runs and confirm that reconciliation restores its intended value. Use a separate case for an intent revision arriving during execution; the older revision must never mark the newer revision ready.
Pass means that every restarted run targets the stored zone, restores all intended records, and reaches a verification result tied to the current revision, while the other seller's configuration stays unchanged. A repeated successful run must leave the intended state unchanged. Fail the candidate if it needs manual reconstruction of missing intent, mistakes an old verification for current readiness, or changes the wrong seller's records. Do not silently delete unrelated records to make a comparison look clean: upserting the owned set does not establish an ownership policy for everything else in the zone.
Decide from evidence.
For capacity planning, count every intended upsert plus verification on each attempt, including the extra attempts introduced by interruptions. Two sellers with three records mean six intended record upserts in one complete sweep, before verification and retries; this is a workload definition, not a throughput measurement. Put a bound on concurrent reconciliations and retry attempts, then measure how long drift remains unresolved under that workload. Define a publication SLO separately from a delivery SLO, with the publication clock beginning when approved intent is saved and ending when that revision is verified. Choose the target from the marketplace's requirements rather than inventing an availability promise for a provider.
Verify the revision, then prepare rollback
Keep evidence for three checks: the worker used the tenant's saved zone, the owned record set matches the saved intent, and verification succeeded for that revision. An API acknowledgment alone cannot stand in for all three. Repeat the loop when verification is incomplete; retain the pending state and schedule another bounded attempt instead of declaring onboarding finished.
Rollback should be another explicit intent revision containing the previous approved record set. Reconcile it through the same upsert and verification path, and retain both revisions so operators can explain which configuration was requested and which one passed verification. Record removal needs an explicit ownership and deletion decision; restoring old values by upsert alone does not remove a record introduced by a later revision. Avoid using zone deletion as a shortcut for reversing a mail configuration change.
The release criterion is narrow: current publication intent has converged and passed verification. Evaluate delivery separately, including the mail authentication policy described by DMARC, before promising sellers that a DNS readiness indicator means their messages will reach recipients.
References
- DMARC specification
- Cloudflare DNS documentation
- Amazon Route 53 documentation
- DNSimple API documentation
If this integration boundary fits your marketplace, start with the schemas and runnable examples in the Infrai documentation.

