Worker System Architecture
Crane Ledger uses a hybrid approach to API operations, combining immediate responses for simple operations with background processing for complex tasks. This guide explains when operations are processed synchronously vs asynchronously, and how to handle each type effectively.
Operation Types
Synchronous Operations (Direct Response)
Most read operations and simple writes return results immediately. These operations complete within the HTTP request/response cycle.
Characteristics:
- Immediate response with data
- Standard HTTP status codes
- No background processing required
- Predictable response times (< 100ms)
Examples:
// GET operations - always synchronous
const accounts = await craneLedger.accounts.list('org_xxx');
const invoice = await craneLedger.invoices.get('org_xxx', 'inv_xxx');
// Simple POST operations - synchronous
const currency = await craneLedger.currencies.create('org_xxx', {
code: 'EUR',
name: 'Euro'
});
Asynchronous Operations (Worker-Based)
Complex operations that require validation, cross-system coordination, or significant processing are queued for background execution.
Characteristics:
- Immediate acknowledgment with job ID
- Background processing (seconds to minutes)
- Status polling or webhooks for completion
- Higher reliability for complex operations
Examples:
// Complex operations - asynchronous
const result = await craneLedger.contacts.create('org_xxx', {
name: 'New Customer',
contact_type: 'customer'
});
// Returns: { request_id: "req_xxx", status: "queued" }
Worker-Based Operations
Which Operations Use Workers?
| Resource | Create | Update | Delete | Notes |
|---|---|---|---|---|
| Contacts | ✅ Worker | ✅ Worker | ✅ Worker | Business logic validation |
| Invoices | ❌ Direct | ❌ Direct | ❌ Direct | Simple document operations |
| Bills | ✅ Worker | ❌ Direct | ❌ Direct | Financial impact validation |
| Transfers | ✅ Worker | ❌ Direct | ❌ Direct | Double-entry accounting |
| Categories | ✅ Worker | ❌ Direct | ❌ Direct | Relationship validation |
| Taxes | ✅ Worker | ❌ Direct | ❌ Direct | Compliance checking |
| Items | ✅ Worker | ❌ Direct | ❌ Direct | Inventory impact |
Worker Response Format
All worker-based operations return a standardized response:
{
"request_id": "req_xxxxxxxxxxxxxxxx",
"success": true,
"data": {
"status": "queued",
"estimated_completion": "2024-01-15T10:30:05Z"
},
"error": null,
"duration_ms": 45,
"credit_cost": 5
}
Response Fields:
request_id: Unique identifier for tracking the operationstatus: Current status (queued,processing,completed,failed)estimated_completion: Expected completion timecredit_cost: Credits consumed (charged immediately)
Handling Asynchronous Operations
Option 1: Polling for Status
Poll the operation status using the request_id:
async function createContactAndWait(orgId, contactData) {
// Start the operation
const result = await craneLedger.contacts.create(orgId, contactData);
const requestId = result.request_id;
// Poll for completion
while (true) {
try {
// Check operation status (this is a conceptual endpoint)
const status = await craneLedger.operations.getStatus(requestId);
if (status.status === 'completed') {
return status.result; // The created contact
} else if (status.status === 'failed') {
throw new Error(status.error);
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
// Handle polling errors
console.error('Status check failed:', error);
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
}
Option 2: Webhook Notifications
Configure webhooks to receive completion notifications:
// Webhook endpoint receives operation completion
app.post('/webhooks/craneledger', (req, res) => {
const { request_id, status, result, error } = req.body;
if (status === 'completed') {
console.log(`Operation ${request_id} completed:`, result);
// Process the result
updateLocalDatabase(result);
} else if (status === 'failed') {
console.error(`Operation ${request_id} failed:`, error);
// Handle the error
notifyUserOfFailure(error);
}
res.sendStatus(200);
});
Option 3: Fire-and-Forget
For operations where you don't need immediate confirmation:
async function bulkImportContacts(orgId, contacts) {
const results = [];
for (const contact of contacts) {
try {
const result = await craneLedger.contacts.create(orgId, contact);
results.push({
request_id: result.request_id,
status: 'queued'
});
} catch (error) {
results.push({
contact: contact.name,
error: error.message
});
}
// Small delay to avoid overwhelming the API
await new Promise(resolve => setTimeout(resolve, 100));
}
return results;
}
Best Practices
Error Handling
async function robustOperation(operation, ...args) {
try {
const result = await operation(...args);
if (result.request_id) {
// Asynchronous operation
return {
type: 'async',
request_id: result.request_id,
status: 'queued'
};
} else {
// Synchronous operation
return {
type: 'sync',
data: result
};
}
} catch (error) {
// Handle API errors
if (error.response?.status === 429) {
// Rate limited - retry with backoff
await delay(getBackoffDelay());
return robustOperation(operation, ...args);
}
throw error;
}
}
Rate Limiting Considerations
class ApiClient {
constructor() {
this.queue = [];
this.processing = false;
}
async enqueue(operation, ...args) {
return new Promise((resolve, reject) => {
this.queue.push({ operation, args, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const { operation, args, resolve, reject } = this.queue.shift();
try {
const result = await operation(...args);
resolve(result);
} catch (error) {
if (error.response?.status === 429) {
// Rate limited - put back in queue with delay
setTimeout(() => {
this.queue.unshift({ operation, args, resolve, reject });
this.processing = false;
}, 60000); // Wait 1 minute
return;
}
reject(error);
}
// Small delay between operations
await new Promise(resolve => setTimeout(resolve, 100));
}
this.processing = false;
}
}
Idempotency
For critical operations, implement idempotency:
async function createContactIdempotent(orgId, contactData, idempotencyKey) {
// Use idempotency key to prevent duplicates
const headers = {
'X-Idempotency-Key': idempotencyKey
};
try {
const result = await craneLedger.contacts.create(orgId, contactData, { headers });
if (result.request_id) {
// Store mapping for status checking
await storeIdempotencyMapping(idempotencyKey, result.request_id);
}
return result;
} catch (error) {
if (error.response?.status === 409) {
// Idempotency key already used
const existingRequestId = await getIdempotencyMapping(idempotencyKey);
return await craneLedger.operations.getStatus(existingRequestId);
}
throw error;
}
}
Operation Status Checking
While there's no direct status checking endpoint yet, you can implement status checking by:
- Database Polling: If you have access to the same database
- Event Monitoring: Watch for completion events
- Webhook Tracking: Rely on webhook notifications
- Time-based Estimation: Assume completion after estimated time
Performance Considerations
Synchronous Operations
- Fast: < 100ms response time
- Predictable: Consistent performance
- Resource Light: Minimal server resources
- Cacheable: Results can be cached
Asynchronous Operations
- Scalable: Handle complex operations without blocking
- Reliable: Retry failed operations automatically
- Resource Intensive: Background processing required
- Monitoring Required: Need to track completion status
Migration Strategies
From Synchronous to Asynchronous
// Before: Assuming all operations are synchronous
async function createInvoice(orgId, invoiceData) {
const invoice = await api.invoices.create(orgId, invoiceData);
return sendEmail(invoice);
}
// After: Handling both sync and async operations
async function createInvoice(orgId, invoiceData) {
const result = await api.invoices.create(orgId, invoiceData);
if (result.request_id) {
// Async operation - wait for completion or use webhooks
const invoice = await waitForCompletion(result.request_id);
return sendEmail(invoice);
} else {
// Sync operation - use immediately
return sendEmail(result);
}
}
Building Resilient Applications
class ResilientApiClient {
constructor(apiClient, options = {}) {
this.apiClient = apiClient;
this.retryAttempts = options.retryAttempts || 3;
this.backoffMs = options.backoffMs || 1000;
}
async execute(operation, ...args) {
let lastError;
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
try {
const result = await operation.call(this.apiClient, ...args);
// Handle async operations
if (result.request_id) {
return await this.handleAsyncOperation(result);
}
return result;
} catch (error) {
lastError = error;
if (this.isRetryable(error) && attempt < this.retryAttempts) {
await this.delay(this.backoffMs * Math.pow(2, attempt - 1));
continue;
}
break;
}
}
throw lastError;
}
async handleAsyncOperation(result) {
// Implementation for handling async operations
// Could poll, use webhooks, or return request_id
return result;
}
isRetryable(error) {
const retryableStatuses = [429, 500, 502, 503, 504];
return retryableStatuses.includes(error.response?.status);
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Monitoring and Debugging
Track Operation Performance
class OperationTracker {
constructor() {
this.operations = new Map();
}
start(operationId, operation, args) {
this.operations.set(operationId, {
id: operationId,
operation,
args,
startTime: Date.now(),
status: 'started'
});
}
complete(operationId, result) {
const op = this.operations.get(operationId);
if (op) {
op.endTime = Date.now();
op.duration = op.endTime - op.startTime;
op.status = 'completed';
op.result = result;
this.logOperation(op);
}
}
fail(operationId, error) {
const op = this.operations.get(operationId);
if (op) {
op.endTime = Date.now();
op.duration = op.endTime - op.startTime;
op.status = 'failed';
op.error = error;
this.logOperation(op);
}
}
logOperation(op) {
console.log(`Operation ${op.operation} (${op.id}): ${op.status} in ${op.duration}ms`);
if (op.error) {
console.error('Error:', op.error);
}
}
}
Summary
Understanding Crane Ledger's hybrid operation model is crucial for building reliable applications:
- Synchronous operations provide immediate results for simple tasks
- Asynchronous operations handle complex business logic reliably
- Proper error handling ensures resilient application behavior
- Monitoring and tracking help maintain system health
Choose the appropriate pattern based on your application's needs, and implement robust error handling and status tracking for the best user experience.
Need help?
Create a free account to access our support portal. Once signed in, use the Support tab in your dashboard to submit a support ticket — our team typically responds within 24 hours.
- ✨ For LLMs/AI assistants: Read our structured API reference