ਇੱਕ ਯੂਜ਼ਰ ਬਟਨ 'ਤੇ ਕਲਿੱਕ ਕਰਦਾ ਹੈ। ਰਿਕਵੈਸਟ (request) ਰੁਕ ਜਾਂਦੀ ਹੈ। ਦਸ ਸੈਕਿੰਡ ਦੀ ਚੁੱਪ। ਉਹ ਫਾਲਬੈਕ (fallback) ਬਟਨ 'ਤੇ ਕਲਿੱਕ ਕਰਦਾ ਹੈ। ਹੁਣ ਇੱਕੋ ਇਰਾਦੇ (intention) ਲਈ ਦੋ ਕੰਮ (jobs) ਚੱਲ ਰਹੇ ਹਨ। ਨਤੀਜੇ ਵਜੋਂ ਡੁਪਲੀਕੇਟ ਸਾਈਡ ਇਫੈਕਟਸ (side effects), ਦੋਹਰੇ ਚਾਰਜ, ਅਤੇ ਡੇਟਾ ਦਾ ਅਜਿਹਾ ਹਫੜਾ-ਦਫੜੀ ਹੁੰਦਾ ਹੈ ਜੋ ਤੁਹਾਡੀ ਪੂਰੀ ਦੁਪਹਿਰ ਖਰਾਬ ਕਰ ਸਕਦੀ ਹੈ।

ਇਹ ਫਰੰਟਐਂਡ (frontend) ਬੱਗ ਨਹੀਂ ਹੈ। React ਵਿੱਚ ਇੱਕ ਡਿਸਏਬਲਡ ਬਟਨ ਜਾਂ ਡੀਬਾਊਂਸ ਟਾਈਮਰ (debounce timer) ਤੁਹਾਨੂੰ ਨਹੀਂ ਬਚਾ ਸਕਦਾ। ਪਹਿਲੀ ਰਿਕਵੈਸਟ ਪਹਿਲਾਂ ਹੀ ਚੱਲ ਰਹੀ ਸੀ। ਨੈੱਟਵਰਕ ਨੇ ਸਿਰਫ਼ ਰਿਸਪਾਂਸ (response) ਨੂੰ ਨਿਗਲ ਲਿਆ। ਜੇਕਰ ਤੁਹਾਡਾ ਬੈਕਐਂਡ (backend) ਹਰ ਆਉਣ ਵਾਲੀ ਰਿਕਵੈਸਟ ਨੂੰ ਇੱਕ ਬਿਲਕੁਲ ਨਵਾਂ ਨਿਰਦੇਸ਼ ਮੰਨਦਾ ਹੈ, ਤਾਂ ਰੀਟ੍ਰਾਈਜ਼ (retries) ਮੁਸੀਬਤ ਬਣ ਜਾਂਦੀਆਂ ਹਨ। ਤੁਹਾਨੂੰ ਇਸਨੂੰ ਆਪਣੇ API ਡਿਜ਼ਾਈਨ ਅਤੇ ਡੇਟਾਬੇਸ ਸਕੀਮਾ (database schema) ਵਿੱਚ ਠੀਕ ਕਰਨ ਦੀ ਲੋੜ ਹੈ।

ਇਸਦਾ ਹੱਲ ਇੱਕ ਸਧਾਰਨ ਸੰਰਚਨਾਤਮਕ ਵੰਡ (structural split) ਨਾਲ ਸ਼ੁਰੂ ਹੁੰਦਾ ਹੈ।

Jobs ਨੂੰ Attempts ਤੋਂ ਵੱਖ ਕਰੋ

ਇੱਕ job ਨੂੰ ਉਸ ਚੀਜ਼ ਦੇ ਟਿਕਾਊ ਰਿਕਾਰਡ ਵਜੋਂ ਸਮਝੋ ਜੋ ਯੂਜ਼ਰ ਚਾਹੁੰਦਾ ਹੈ। ਇਹ ਮਾਲਕ (owner), ਪੈਰਾਮੀਟਰਾਂ (parameters), ਟਾਰਗੇਟ ਪ੍ਰੋਵਾਈਡਰ (target provider), ਅਤੇ ਸਹੀ ਇਰਾਦੇ (intent) ਨੂੰ ਕੈਪਚਰ ਕਰਦਾ ਹੈ। ਇੱਕ attempt ਉਸ ਇਰਾਦੇ ਨੂੰ ਪੂਰਾ ਕਰਨ ਦੀ ਇੱਕ ਖਾਸ ਕੋਸ਼ਿਸ਼ ਹੈ।

ਇੱਕ ਪ੍ਰਿੰਟ ਸ਼ਾਪ ਦੀ ਕਲਪਨਾ ਕਰੋ। ਤੁਸੀਂ ਇੱਕ ਫਾਈਲ ਸੌਂਪਦੇ ਹੋ ਅਤੇ ਉਹ ਤੁਹਾਨੂੰ ਟਿਕਟ #45 ਦਿੰਦੇ ਹਨ। ਉਹ ਟਿਕਟ job ਹੈ। ਸ਼ਾਪ ਇਨਕਜੈੱਟ ਪ੍ਰਿੰਟਰ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰਦੀ ਹੈ। ਇਹ ਫਸ ਜਾਂਦਾ ਹੈ। ਇਹ ਪਹਿਲੀ attempt ਹੈ। ਉਹ ਫਾਈਲ ਨੂੰ ਲੇਜ਼ਰ ਪ੍ਰਿੰਟਰ ਵਿੱਚ ਬਦਲ ਦਿੰਦੇ ਹਨ। ਇਹ ਦੂਜੀ attempt ਹੈ। ਪੂਰੀ ਪ੍ਰਕਿਰਿਆ ਦੌਰਾਨ, ਟਿਕਟ #45 ਕਦੇ ਨਹੀਂ ਬਦਲਦੀ। ਜੇਕਰ ਸ਼ਾਪ ਹਰ ਪ੍ਰਿੰਟਰ ਲਈ ਇੱਕ ਨਵੀਂ ਟਿਕਟ ਜਾਰੀ ਕਰਦੀ, ਤਾਂ ਤੁਸੀਂ ਤਿੰਨ ਵਾਰ ਭੁਗਤਾਨ ਕਰਦੇ ਅਤੇ ਤਿੰਨ ਅਣਚਾਹੇ ਕਾਪੀਆਂ ਪ੍ਰਾਪਤ ਕਰਦੇ।

ਤੁਹਾਡਾ ਡੇਟਾਬੇਸ ਵੀ ਇਸੇ ਤਰ੍ਹਾਂ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ। ਇੱਕ ਟੇਬਲ jobs ਨੂੰ ਰੱਖਦਾ ਹੈ। ਦੂਜਾ ਟੇਬਲ attempts ਨੂੰ ਰੱਖਦਾ ਹੈ। Job ਰੋਅ (row) ਸਥਿਰ ਰਹਿੰਦੀ ਹੈ ਜਦੋਂ ਕਿ attempts ਉਸਦੇ ਹੇਠਾਂ ਇਕੱਠੀਆਂ ਹੁੰਦੀਆਂ ਜਾਂਦੀਆਂ ਹਨ।

ਇਹ ਵੱਖਰੇਵਾਂ ਤੁਹਾਨੂੰ ਕੰਟਰੋਲ ਦਿੰਦਾ ਹੈ। ਇਹ ਤੁਹਾਨੂੰ ਇੱਕ idempotency key ਲਗਾਉਣ ਲਈ ਇੱਕ ਜਗ੍ਹਾ ਵੀ ਦਿੰਦਾ ਹੈ ਜੋ ਨੈੱਟਵਰਕ ਦੀਆਂ ਖਰਾਬੀਆਂ (blips) ਤੋਂ ਬਾਅਦ ਵੀ ਬਣੀ ਰਹਿੰਦੀ ਹੈ।

ਹਰ Job 'ਤੇ Idempotency Key ਲਾਜ਼ਮੀ ਕਰੋ

ਹਰ POST ਰਿਕਵੈਸਟ ਜੋ ਇੱਕ job ਬਣਾਉਂਦੀ ਹੈ, ਉਸ ਵਿੱਚ ਇੱਕ ਵਿਲੱਖਣ (unique) idempotency key ਹੋਣੀ ਚਾਹੀਦੀ ਹੈ। ਇਹ ਕੀ (key) ਯੂਜ਼ਰ ਦੀ ਹੈ, ਸੈਸ਼ਨ (session) ਦੀ ਨਹੀਂ। Owner ID ਅਤੇ key ਨੂੰ ਜੋੜੋ, ਫਿਰ ਉਹਨਾਂ ਦੋਵਾਂ ਕਾਲਮਾਂ 'ਤੇ ਇੱਕ unique database constraint ਲਾਗੂ ਕਰੋ।

ਡੇਟਾਬੇਸ ਕੰਸਟ੍ਰੇਂਟ (database constraint) ਕਿਉਂ? ਕਿਉਂਕਿ ਇਨਸਰਟ (insert) ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਐਪਲੀਕੇਸ਼ਨ ਕੋਡ ਵਿੱਚ ਮੌਜੂਦਗੀ ਦੀ ਜਾਂਚ ਕਰਨਾ ਇੱਕ race condition ਹੈ ਜੋ ਕਦੇ ਵੀ ਹੋ ਸਕਦੀ ਹੈ। ਦੋ ਇੱਕੋ ਜਿਹੇ ਰਿਕਵੈਸਟ ਇੱਕੋ ਹੀ ਮਾਈਕ੍ਰੋਸੈਕੰਡ ਦੇ ਅੰਤਰ ਵਿੱਚ ਨਿਕਲ ਸਕਦੇ ਹਨ। ਡੇਟਾਬੇਸ ਨੂੰ ਲਾਗੂ ਕਰਨ ਦਿਓ। ਜੇਕਰ ਕੋਈ ਯੂਜ਼ਰ ਇੱਕੋ owner ID ਅਤੇ key ਦੋ ਵਾਰ ਭੇਜਦਾ ਹੈ, ਤਾਂ ਦੂਜੀ ਰਿਕਵੈਸਟ unique violation ਨੂੰ ਫੜ ਲਵੇਗੀ ਅਤੇ ਤੁਸੀਂ ਮੌਜੂਦਾ job ਵਾਪਸ ਕਰ ਦਿਓਗੇ। ਦੋਵਾਂ ਰਿਕਵੈਸਟਾਂ ਨੂੰ ਇੱਕੋ ਜਿਹਾ job ID ਮਿਲੇਗਾ। ਕੋਈ ਡੁਪਲੀਕੇਟ ਕੰਮ ਸ਼ੁਰੂ ਨਹੀਂ ਹੋਵੇਗਾ।

ਸਕੋਪ (scope) ਬਾਰੇ ਸਖ਼ਤ ਰਹੋ। ਜੇਕਰ ਕੋਈ ਕੀ (key) ਦੀ ਮੁੜ ਵਰਤੋਂ ਕਰਦਾ ਹੈ ਪਰ ਇਨਪੁਟ ਪੇਲੋਡ (input payload) ਬਦਲ ਦਿੰਦਾ ਹੈ, ਤਾਂ conflict ਵਾਪਸ ਕਰੋ। Idempotency key ਨੂੰ ਸਿਰਫ਼ ਯੂਜ਼ਰ ਨਾਲ ਹੀ ਨਹੀਂ, ਸਗੋਂ ਇੱਕ ਸਹੀ ਇਰਾਦੇ (exact intention) ਨਾਲ ਜੁੜਿਆ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ। ਵੱਖਰੇ ਇਨਪੁਟ ਦੇ ਨਾਲ ਇੱਕੋ ਹੀ ਕੀ ਦਾ ਮਤਲਬ ਹੈ ਕਿ ਕਲਾਇੰਟ ਉਲਝਣ ਵਿੱਚ ਹੈ, ਅਤੇ ਤੁਹਾਡੇ ਸਿਸਟਮ ਨੂੰ ਅੰਦਾਜ਼ਾ ਲਗਾਉਣ ਦੀ ਬਜਾਏ ਇਸਨੂੰ ਰੱਦ ਕਰ ਦੇਣਾ ਚਾਹੀਦਾ ਹੈ।

State Transitions ਦੀ ਰੱਖਿਆ ਕਰੋ

ਇੱਕ attempt ਇੱਕ state transition ਹੈ, ਨਵਾਂ job ਨਹੀਂ। ਤੁਹਾਡੇ API ਨੂੰ ਇੱਕ ਨਵਾਂ attempt ਸ਼ੁਰੂ ਕਰਨ ਤੋਂ ਇਨਕਾਰ ਕਰ ਦੇਣਾ ਚਾਹੀਦਾ ਹੈ ਜੇਕਰ ਪਿਛਲਾ attempt ਅਜੇ ਵੀ ਸ਼ੁਰੂਆਤੀ ਜਾਂ ਅਣਜਾਣ (unknown) ਸਟੇਟ ਵਿੱਚ ਫਸਿਆ ਹੋਇਆ ਹੈ।

ਟਾਈਮਆਊਟ (Timeouts) ਇਸਦਾ ਕਾਰਨ ਹਨ। ਜਦੋਂ ਇੱਕ ਪ੍ਰੋਵਾਈਡਰ ਰਿਕਵੈਸਟ ਟਾਈਮਆਊਟ ਹੋ ਜਾਂਦੀ ਹੈ, ਤਾਂ ਕਲਾਇੰਟ ਨੂੰ ਅਸਫਲਤਾ (failure) ਦਿਖਾਈ ਦਿੰਦੀ ਹੈ, ਪਰ ਸਰਵਰ-ਸਾਈਡ ਪ੍ਰਕਿਰਿਆ ਅਜੇ ਵੀ ਚੱਲ ਰਹੀ ਹੋ ਸਕਦੀ ਹੈ। GPU ਕਲੱਸਟਰ ਅਜੇ ਵੀ ਤੁਹਾਡੀ inference ਰਿਕਵੈਸਟ 'ਤੇ ਕੰਮ ਕਰ ਰਿਹਾ ਹੋ ਸਕਦਾ ਹੈ। ਕੰਟੇਨਰ (container) ਅਜੇ ਵੀ blob storage ਵਿੱਚ ਲਿਖ ਰਿਹਾ ਹੋ ਸਕਦਾ ਹੈ। ਜੇਕਰ ਤੁਸੀਂ ਟਾਈਮਆਊਟ ਹੋਏ attempt ਨੂੰ ਅਸਫਲ (failed) ਵਜੋਂ ਮਾਰਕ ਕਰਦੇ ਹੋ ਅਤੇ ਤੁਰੰਤ ਦੂਜੀ attempt ਸ਼ੁਰੂ ਕਰਦੇ ਹੋ, ਤਾਂ ਤੁਸੀਂ ਡੁਪਲੀਕੇਟ ਸਾਈਡ

This handles the reverse-order finish cleanly. Attempt A leaves first but returns after thirty seconds. Attempt B leaves second but returns after five seconds. Attempt B wins the compare-and-swap. Attempt A’s update touches zero rows. Your system logs the race, ignores the stale payload, and moves on.

Test the Breakpoints

You will not catch these bugs in happy-path testing. Your suite needs to target the fractures.

  • Simulate a double-click. Two simultaneous POST requests with the same idempotency key must return identical job IDs.
  • Send the same key with mismatched input. Expect a conflict response. The system must not silently return the existing job if the parameters differ.
  • Provoke a timeout. Verify the job lands in an unknown state, not a failed state, and that the system blocks further attempts until the ambiguity clears.
  • Force two attempts to finish in reverse order. Confirm that the second one to return loses, even if the first one to leave was the official primary provider.

These tests are not edge-case luxuries. They are the contract your API makes with the rest of the system.

Validate Provider Intent Before You Fail Over

If you run a multi-provider setup, you might be tempted to treat different AI models as interchangeable slots. They share the same code path, the same HTTP client, and the same JSON schema. That does not mean they behave the same.

One model might hallucinate a top-level key. Another might ignore your system prompt formatting. Schema validation catches syntax errors, but it will pass a response that your business logic cannot interpret. A provider might return valid JSON that simply does the wrong thing with your prompt template.

Run provider-specific tests before you allow automatic model switching. Confirm that the fallback model actually respects your output structure at low temperature. Verify that your prompt renders correctly through that provider’s tokenizer. Test the full round trip with real inputs. Automatic failover is only safe when you have proven that the fallback shares the same operational contract.

Keep One Job Per Intent

Fallback paths are good. Uncontrolled fallback multiplication is a bug. Every layer of your stack needs to evaluate whether it has already seen the exact task. The load balancer, the API handler, the database, and the worker must all respect the same identity.

Build your system so that retries and fallbacks surface as new attempts under one stable job. Lock the job down with a database-backed idempotency key. Guard the transitions. Race the attempts. Let exactly one win. That is how you keep a single user click from turning into a weekend of data cleanup.