In one company, one person often had two accounts. The mini program and website each had their own backend, user table, and registration flow. The goal was to recognize the same person without making either product dependent on the other.

I rejected merging the user tables for one root reason: cost. The two systems used different stacks, schemas, and registration logic. A unified table would require reconciling every field and every write path, while historical duplicates and nulls turned migration into an irreversible project. The long-term cost was worse: a shared database would create a shared failure domain, and redundancy would reintroduce synchronization and CAP trade-offs. For a small team, weak coupling was cheaper to build and operate.

My hard boundary was fault isolation, not perfect consistency. A phone number remained contact information, not sufficient proof of identity. Separate numeric IDs, issued in separate ranges, linked the registries. Neither side was the other’s master. I could accept temporary profile differences and reconcile them later; I could not accept one system’s failure cascading into the other.

Weakly coupled dual-system architecture: two independent user systems, an outbox for real-time push, nightly two-way reconciliation, and a shared visible degradation ledger

Fig. 1: Each system keeps its own user table; an outbox and a nightly reconciliation network exchange data, while the degradation ledger is shared for visibility, not as a master database.

Failure policy followed business cost. Registration failed open because blocking a high-frequency entry point would stop the business. Rebinding failed closed because a mismatched phone login could associate a user with the wrong account. Conflicts that rules could not resolve entered a highly visible degradation ledger for human handling.

Consistency used an outbox plus nightly reconciliation. Updates were narrow, so nulls never overwrote values. Migration used a three-step backfill with checksums and row-count gates. Batch execution could continue after an individual failure, but no failure was allowed to disappear silently: every exception had to be located and accounted for. INSERT IGNORE was rejected because skipped rows could otherwise look like success.

Failure posture matrix: registration fails open, rebinding fails closed, back-office lookup is greyed out, and batch execution may continue after a single failure but every failure must be located, accounted for, and visible

Fig. 2: Different links carry different costs of error; a batch may continue after one failure, but no failure is allowed to vanish.

On August 30, 2026, the link went live with about twenty minutes of downtime and no rollback. The first reconciliation required zero repairs. Ticket reissue then triggered 429s because the job had no throttle. We rejected both a one-sided fix and a batch API with complicated partial-success semantics. Symmetric throttling, inbound limit adjustments, and concurrency protection brought the next night to zero. If I redesigned it, rate budgets and backoff would be first-version requirements.

I prepared the website-side implementation; a colleague merged and maintains it. Login and pages remained their work. The scale was small: about a hundred rows per registry and a dozen linked users, not high-concurrency infrastructure. Consistency is eventual, phone numbers remain weak identity evidence, and I cannot claim long-term zero incidents after leaving.

The design’s value is not that two systems look like one. It lets them remain independent, with failures that stay explainable and reversible.

This is a summary; the full case study is in Chinese: read the Chinese version.