How do you design an API that supports both synchronous and asynchronous operations, and what are the trade-offs?

Atlassian Backend Developer 3–5 Years API Design

Designing an API to support both synchronous and asynchronous operations requires careful consideration of latency, resource usage, and client expectations. Synchronous operations, best suited for quick, atomic actions like fetching a user profile or submitting a small form, provide an immediate response. The client waits for the server to complete the task and return the result within the same request-response cycle. This is simple for clients to implement but can lead to timeouts or poor user experience if the operation takes too long.

Hybrid API approaches

Asynchronous operations are ideal for long-running or resource-intensive tasks, such as generating a large report, processing a video, or initiating a complex workflow. Here, the API immediately returns an acknowledgment (typically a 202 Accepted status) along with a job ID or a URI to poll for status updates. The actual work is then performed by a separate background process, often managed via a message queue and worker system. Clients can poll the status endpoint periodically or, in more advanced scenarios, use webhooks or WebSockets for real-time notifications when the job completes. The trade-off is increased client complexity and the need for robust backend job scheduling and monitoring.

Best practice

A best practice is to clearly define which operations are synchronous and which are asynchronous based on their expected execution time. For synchronous endpoints, aim for response times under 500ms. For asynchronous operations, provide clear documentation on how to initiate the job, retrieve the job ID, and poll for status or register for callbacks. Consistent error handling across both types of operations is crucial, though asynchronous errors might manifest as a job status change rather than an immediate HTTP error code.

Edge case interviewers probe for

Interviewers might ask about idempotency for asynchronous operations. Since clients might retry initiating a long-running job, your API should ideally handle duplicate requests without creating duplicate work or inconsistent state. This often involves using a unique request ID provided by the client, or generating one internally and checking its status before re-queueing the task.

Common mistake

A common mistake is treating all operations as synchronous, leading to API timeouts, unresponsive clients, and resource exhaustion on the server for long-running tasks. Another error is designing asynchronous APIs without clear status tracking or notification mechanisms, leaving clients guessing about the outcome of their requests. Not providing a robust retry mechanism with exponential backoff for polling clients can also degrade system performance.

What the interviewer is checking

The interviewer is checking your understanding of API design principles, system scalability, and fault tolerance. They want to see if you can identify appropriate patterns for different types of operations, handle client expectations, manage backend resources efficiently, and anticipate potential issues like timeouts, retries, and state consistency across distributed systems.

Imagine you are at a fast-food restaurant. A synchronous operation is like ordering a coffee: you tell the cashier what you want, you wait right there, and they hand you the coffee immediately. You get your result in that same interaction. This works great for quick tasks where you expect an instant outcome.

Now, an asynchronous operation is like ordering a custom cake: you tell the bakery what you want, they give you a receipt with a number, and you go home. They tell you to come back tomorrow or that they’ll call you when it’s ready. You don’t wait around; you go do other things. Later, you check your receipt number, or they call you, and you get your cake. This is perfect for things that take a long time to prepare, so you aren’t stuck waiting.

Why interviewers ask this

This question assesses your ability to design robust, scalable, and user-friendly APIs that handle diverse operational requirements. It reveals your understanding of performance, resource management, and client interaction patterns in distributed systems.

What a strong answer signals

A strong answer demonstrates a practical understanding of when to use each pattern, how to implement them (HTTP status codes, message queues, webhooks), and the associated architectural complexities and benefits for both client and server.

Common follow-ups

  • How would you handle error reporting and notifications for asynchronous jobs?
  • What strategies would you use for client-side polling to avoid overwhelming the server?
  • Describe how message queues (e.g., Kafka, RabbitMQ) fit into an asynchronous API design.

Advanced variation

Design a system where a client can initiate a complex multi-step workflow asynchronously, receive real-time updates on each step’s progress, and potentially cancel the workflow mid-execution through the API.

Consider an e-commerce platform where users can upload a large catalog file for bulk product updates. A synchronous API endpoint for this would likely time out, frustrate the user, and tie up server resources. Instead, an asynchronous API would accept the file, immediately return a job ID (202 Accepted), and enqueue the processing task into a message queue. A dedicated worker service would then pick up the task, parse the file, validate products, and update the database in the background. The user could then check a “My Uploads” page, which polls a status API using the job ID to see when their catalog update is complete and if any errors occurred.

api_service.py
from flask import Flask, jsonify, request
import uuid
import threading
import time

app = Flask(__name__)
# Simulate a job queue, use a real one like Celery/Kafka in production
_job_store = {} # In-memory store for job statuses

def process_heavy_task(job_id, payload):
    # Simulate a long-running, resource-intensive operation
    time.sleep(5)
    _job_store[job_id]["status"] = "COMPLETED"
    _job_store[job_id]["result"] = f"Data '{payload}' processed."

@app.route('/api/v1/data/async', methods=['POST'])
def create_data_async():
    if not request.json or 'payload' not in request.json:
        return jsonify({"error": "Missing payload"}), 400
    payload = request.json['payload']
    job_id = str(uuid.uuid4())
    _job_store[job_id] = {"status": "PENDING", "payload": payload, "result": None}
    # Kick off background task, then return job ID immediately
    threading.Thread(target=process_heavy_task, args=(job_id, payload)).start()
    return jsonify({
        "jobId": job_id,
        "status": "PENDING",
        "checkStatusUrl": f"/api/v1/job/status/{job_id}"
    }), 202 # 202 Accepted

@app.route('/api/v1/job/status/<job_id>', methods=['GET'])
def get_job_status(job_id):
    job = _job_store.get(job_id)
    if not job:
        return jsonify({"error": "Job not found"}), 404
    return jsonify({
        "jobId": job_id,
        "status": job["status"],
        "result": job["result"]
    }), 200
Client Sync API Request DB Op Result Response Async API Request Message Queue Enqueue Worker Process Database Update DB 202 Accepted + Job ID Poll Status
  1. 1Choose synchronous for fast, atomic operations that require immediate results.
  2. 2Opt for asynchronous for long-running tasks to prevent timeouts and enhance user experience.
  3. 3Asynchronous APIs typically return a 202 Accepted status with a job ID for status tracking.
  4. 4Use message queues and background workers to manage asynchronous task execution and decouple services.
  5. 5Consider idempotency for retried asynchronous requests to ensure consistent state and avoid duplicate work.