Core Entities
Book:
book_id, isbn, title
author_ids: array of author_ids
publisher_id
genre_tags: array of strings
description, language, year
DigitalCopy:
copy_id, book_id
format: EPUB | PDF | MOBI
file_key (S3 object key)
drm_type: ADOBE | LOAN_TOKEN | NONE
total_licenses -- max concurrent loans
available_licenses -- decremented on borrow
BorrowRecord:
record_id, copy_id, patron_id
status: ACTIVE | RETURNED | OVERDUE
borrowed_at, due_at, returned_at
download_token -- signed JWT for file access
Reservation:
reservation_id, copy_id, patron_id
position_in_queue
created_at, notified_at, expires_at
License Management
Unlike physical books, digital copies can support multiple simultaneous loans up to a licensed limit. The atomic checkout pattern prevents overselling:
def borrow_book(copy_id, patron_id, loan_days=14):
with db.transaction():
# Lock the row to prevent concurrent borrows
copy = db.query(
"SELECT * FROM digital_copies WHERE copy_id=? FOR UPDATE",
copy_id
)[0]
if copy['available_licenses'] <= 0:
# Add to reservation queue
add_to_queue(copy_id, patron_id)
return None, "Added to waitlist"
db.execute(
"UPDATE digital_copies SET available_licenses = available_licenses - 1 WHERE copy_id=?",
copy_id
)
due_at = now() + timedelta(days=loan_days)
token = generate_loan_token(copy_id, patron_id, due_at)
record = create_borrow_record(copy_id, patron_id, due_at, token)
return record, "Success"
def return_book(record_id):
with db.transaction():
record = get_borrow_record(record_id)
update_borrow_record(record_id, status='RETURNED', returned_at=now())
db.execute(
"UPDATE digital_copies SET available_licenses = available_licenses + 1 WHERE copy_id=?",
record['copy_id']
)
notify_next_in_queue(record['copy_id'])
SELECT FOR UPDATE is the key: it acquires a row-level lock so only one transaction can decrement available_licenses at a time. Without it, two simultaneous borrows could both read available_licenses=1 and both succeed, creating an overcommitment.
Loan Token DRM
For libraries that cannot afford Adobe DRM infrastructure, a loan token approach using signed JWTs provides reasonable protection:
def generate_loan_token(copy_id, patron_id, due_at):
payload = {
"copy_id": copy_id,
"patron_id": patron_id,
"exp": due_at.timestamp(), # JWT standard expiry claim
"iat": now().timestamp(),
"jti": str(uuid4()) # unique token ID for revocation
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
def validate_loan_token(token):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
# Check revocation list (for early returns)
if is_revoked(payload['jti']):
return None, "Token revoked"
return payload, "Valid"
except jwt.ExpiredSignatureError:
return None, "Loan expired"
The token is embedded in the ebook file or passed to the reader app at download time. When the patron opens the book, the reader app validates the token against the library server. Once exp passes, the library server rejects the token and the reader locks the content.
On early return: add the jti to a revocation list (Redis set) and increment available_licenses. The token is cryptographically invalid after revocation even if the expiry has not passed.
Reservation Queue
When all licenses are checked out, patrons join a queue. On return, the first patron in line gets a notification and a time-limited claim window:
def add_to_queue(copy_id, patron_id):
position = db.query(
"SELECT COALESCE(MAX(position_in_queue), 0) + 1 FROM reservations "
"WHERE copy_id=? AND expires_at IS NULL", copy_id
)[0][0]
db.execute(
"INSERT INTO reservations (copy_id, patron_id, position_in_queue, created_at) "
"VALUES (?, ?, ?, ?)", copy_id, patron_id, position, now()
)
def notify_next_in_queue(copy_id):
next_res = db.query(
"SELECT * FROM reservations WHERE copy_id=? AND notified_at IS NULL "
"ORDER BY position_in_queue LIMIT 1", copy_id
)
if not next_res:
return
res = next_res[0]
expires_at = now() + timedelta(hours=24)
db.execute(
"UPDATE reservations SET notified_at=?, expires_at=? WHERE reservation_id=?",
now(), expires_at, res['reservation_id']
)
send_notification(res['patron_id'], copy_id, expires_at)
# Cron job: expire unclaimed reservations and advance queue
def expire_reservations():
expired = db.query(
"SELECT * FROM reservations WHERE expires_at < ? AND status='NOTIFIED'", now()
)
for res in expired:
db.execute("UPDATE reservations SET status='EXPIRED' WHERE reservation_id=?",
res['reservation_id'])
notify_next_in_queue(res['copy_id'])
Search and Catalog
Full-text search via Elasticsearch. Index schema:
{
"mappings": {
"properties": {
"title": {"type": "text", "analyzer": "english"},
"authors": {"type": "text", "analyzer": "english"},
"description": {"type": "text", "analyzer": "english"},
"genre_tags": {"type": "keyword"},
"language": {"type": "keyword"},
"year": {"type": "integer"},
"available_licenses": {"type": "integer"}
}
}
}
Query with facets and availability boost:
def search_catalog(query, genre=None, language=None, available_only=False):
must = [{"multi_match": {
"query": query,
"fields": ["title^3", "authors^2", "description"],
"type": "best_fields"
}}]
filters = []
if genre:
filters.append({"term": {"genre_tags": genre}})
if language:
filters.append({"term": {"language": language}})
if available_only:
filters.append({"range": {"available_licenses": {"gt": 0}}})
body = {
"query": {
"function_score": {
"query": {"bool": {"must": must, "filter": filters}},
"functions": [
# Boost available books
{"filter": {"range": {"available_licenses": {"gt": 0}}},
"weight": 1.5},
# Recency boost - newer books score higher
{"gauss": {"year": {"origin": "now", "scale": "5y", "decay": 0.5}}}
],
"score_mode": "multiply"
}
},
"aggs": {
"genres": {"terms": {"field": "genre_tags"}},
"languages": {"terms": {"field": "language"}},
"years": {"histogram": {"field": "year", "interval": 10}}
}
}
return es.search(index="books", body=body)
The function_score query blends TF-IDF relevance with availability and recency signals. Aggregations power the facet sidebar (genre counts, language breakdown, year histogram) without separate queries.
Scale and Operational Notes
- License sync: keep Elasticsearch
available_licensesin sync via a change-data-capture stream (Debezium on Postgres). Eventual consistency is acceptable for search; the authoritative source is the relational DB with locks. - File delivery: generate pre-signed S3 URLs valid for the loan duration. The URL is embedded in the download token response, not stored in the DB. Revocation works by rejecting the loan token before the signed URL is used.
- Overdue enforcement: a daily cron marks records OVERDUE and can auto-return (incrementing licenses). Some libraries allow renewals – add a renew endpoint that updates
due_atand reissues the token. - Multi-format: one Book can have multiple DigitalCopy rows (EPUB, PDF, MOBI). Each format has its own license pool – common in publisher contracts.
See also: Atlassian Interview Guide
See also: Shopify Interview Guide