Django Interview Q&A

Django Interview Q&A

Explore django interview q&a code snippets and tutorials

Django Interview Q&A

Double-entry ledger: what it is and why fintech uses it

Every movement is recorded as at least two entries (debit and credit) that sum to zero.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ======================================================================
# Q: What is a double-entry ledger and why do fintech companies use it?
#
# Every movement is recorded as at least two entries (debit and credit) that sum
# to zero. Entries are append-only: we never UPDATE a balance, we add a
# reversing entry instead. The balance is the sum of entries (or a cached
# balance updated in the same transaction). It gives a full audit trail and easy
# reconciliation.
# ======================================================================

def transfer(src_id, dst_id, amount: Decimal, ref: str):
    with transaction.atomic():
        # lock in a fixed order to avoid deadlock
        accts = {a.pk: a for a in Account.objects.select_for_update()
                 .filter(pk__in=[src_id, dst_id]).order_by('pk')}
        if accts[src_id].balance < amount:
            raise InsufficientFunds
        tx = LedgerTransaction.objects.create(reference=ref)  # unique ref = idempotency
        Entry.objects.bulk_create([
            Entry(tx=tx, account_id=src_id, amount=-amount),
            Entry(tx=tx, account_id=dst_id, amount=amount)])
        Account.objects.filter(pk=src_id).update(balance=F('balance') - amount)
        Account.objects.filter(pk=dst_id).update(balance=F('balance') + amount)
Django Interview Q&A

Securing webhooks with HMAC signatures and replay protection

HMAC-SHA256 signature of the body together with a timestamp, comparison with hmac.compare_digest (constant time), rejection if the timestamp is old (replay protection), idempotent processing using the event id, a fast …

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# ======================================================================
# Q: How do you secure webhooks (incoming and outgoing)?
#
# HMAC-SHA256 signature of the body together with a timestamp, comparison with
# hmac.compare_digest (constant time), rejection if the timestamp is old (replay
# protection), idempotent processing using the event id, a fast 2xx response and
# processing in Celery.
# ======================================================================

def verify(request, secret: bytes) -> bool:
    ts = request.headers['X-Timestamp']
    if abs(time.time() - int(ts)) > 300:
        return False
    expected = hmac.new(secret, ts.encode() + b'.' + request.body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, request.headers['X-Signature'])
Django Interview Q&A

REST API versioning strategies: URL, header or date-based

URL (/api/v1/): visible, simple, easy for routing and caching.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# ======================================================================
# Q: What are the API versioning strategies? Which one do you pick and why?
#
# URL (/api/v1/): visible, simple, easy for routing and caching. Header/media
# type (Accept: application/vnd.x.v2+json): "cleaner" REST, but harder to debug.
# Date-based (like Stripe): each account is pinned to a version. For B2B
# customers I choose URL versioning. I release a new version only for breaking
# changes. Adding fields is not breaking. I announce deprecation with headers
# (Deprecation, Sunset), track which clients still call the old version, and
# allow time for migration.
# ======================================================================

REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    'ALLOWED_VERSIONS': ['v1', 'v2'], 'DEFAULT_VERSION': 'v1',
}
# in the view
def get_serializer_class(self):
    return OrderV2Serializer if self.request.version == 'v2' else OrderV1Serializer
Django Interview Q&A

Transactional Outbox: publishing events safely with a database change

If I write to the database and then send to the broker, one of the two can fail.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ======================================================================
# Q: How do you safely publish an event together with a database change? (the
# dual-write problem)
#
# If I write to the database and then send to the broker, one of the two can
# fail. Solution: Transactional Outbox. In the same transaction I write the
# order and a row in the outbox table. A relay (Celery beat, a separate worker,
# or CDC with Debezium) reads the outbox and publishes. Consumers are idempotent
# (an inbox table with processed event_ids).
# ======================================================================

with transaction.atomic():
    order = Order.objects.create(...)
    OutboxEvent.objects.create(
        event_type='order.placed', aggregate_id=order.id,
        payload={'order_id': order.id, 'total': str(order.total)})

# relay: every few seconds
for ev in OutboxEvent.objects.select_for_update(skip_locked=True)\
                             .filter(published_at=None)[:100]:
    broker.publish(ev.event_type, ev.payload, message_id=str(ev.id))
    ev.published_at = timezone.now(); ev.save(update_fields=['published_at'])
Django Interview Q&A

Making a Celery task reliable: idempotency, retries and on_commit

The task must be idempotent, because with at-least-once delivery it may run twice.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ======================================================================
# Q: How do you make a Celery task reliable?
#
# The task must be idempotent, because with at-least-once delivery it may run
# twice. I pass IDs, not objects. Retries with exponential backoff and jitter
# only for transient errors. acks_late=True so the message is acknowledged after
# the task finishes. Timeouts (soft/hard time limits). I send the task with
# on_commit, so it does not run before the row has been committed to the
# database.
# ======================================================================

@shared_task(bind=True, acks_late=True,
             autoretry_for=(requests.Timeout, requests.ConnectionError),
             retry_backoff=True, retry_jitter=True, max_retries=5)
def sync_order_to_erp(self, order_id):
    order = Order.objects.get(pk=order_id)
    if order.erp_synced_at:          # idempotency: already done
        return
    erp.push(order)                   # the ERP accepts idempotency key = order.uuid
    Order.objects.filter(pk=order_id).update(erp_synced_at=timezone.now())

transaction.on_commit(lambda: sync_order_to_erp.delay(order.id))