iveIntegerField(default=0)
is_active = models.BooleanField(default=True)
content_json = models.JSONField(default=dict)
draft_session_key = models.CharField(max_length=100, blank=True, null=True)
class Meta:
ordering = ['position', 'sort_order']
def is_draft(self):
return bool(self.draft_session_key)
To support draft previews, store temporary modifications in the user's session. The storefront renderer checks for an active draft key before falling back to published records.
```python
# views.py
from django.shortcuts import render
from django.contrib.sessions.backends.db import SessionStore
def render_storefront(request):
session_key = request.session.session_key
draft_blocks = LayoutBlock.objects.filter(draft_session_key=session_key, is_active=True)
published_blocks = LayoutBlock.objects.filter(draft_session_key__isnull=True, is_active=True)
active_blocks = draft_blocks if draft_blocks.exists() else published_blocks
return render(request, 'storefront/index.html', {'blocks': active_blocks})
Why this works: Decoupling layout from templates removes deployment friction. Session-scoped drafts prevent unreviewed changes from leaking to production while allowing real-time preview validation.
2. Catalog Query Optimization
Product catalogs with attributes, images, and warehouse mappings quickly trigger N+1 queries. Django's prefetch_related and select_related must be applied strategically at the queryset level, not in template loops.
# catalog/queries.py
from django.db.models import Prefetch
from .models import Product, ProductImage, ProductAttribute
def get_catalog_queryset():
return Product.objects.select_related(
'category', 'primary_warehouse'
).prefetch_related(
Prefetch(
'images',
queryset=ProductImage.objects.filter(is_primary=False).order_by('sort_order'),
to_attr='secondary_images'
),
Prefetch(
'attributes',
queryset=ProductAttribute.objects.select_related('definition'),
to_attr='filterable_attributes'
)
).filter(status='active')
Why this works: Prefetching at the ORM level collapses dozens of round-trips into two optimized queries. Attaching results to custom attributes (secondary_images, filterable_attributes) keeps template logic clean and prevents accidental lazy-loading triggers.
3. Transactional Checkout Enforcement
Carts must never finalize based on stale data. Before payment processing, the system re-syncs line items with the live database, re-validates promotional codes, and locks inventory using database-level row locking.
# checkout/processors.py
from django.db import transaction
from django.core.exceptions import ValidationError
from .models import Cart, CartItem, Product, Coupon
def finalize_cart(cart_id: str) -> dict:
with transaction.atomic():
cart = Cart.objects.select_for_update().get(id=cart_id)
# Re-sync prices and stock
for item in cart.items.select_related('product'):
live_product = Product.objects.select_for_update().get(id=item.product_id)
if live_product.stock_quantity < item.quantity:
raise ValidationError(f"Insufficient stock for {live_product.sku}")
item.unit_price = live_product.current_price
item.line_total = item.unit_price * item.quantity
# Re-validate coupon
if cart.coupon_code:
coupon = Coupon.objects.select_for_update().get(code=cart.coupon_code)
if not coupon.is_valid(cart.total_before_discount):
cart.coupon_code = None
cart.save()
# Deduct stock
for item in cart.items.select_related('product'):
item.product.stock_quantity -= item.quantity
item.product.save(update_fields=['stock_quantity'])
return {'status': 'success', 'final_total': cart.total_after_discount}
Why this works: select_for_update() creates a pessimistic lock that prevents concurrent requests from overselling inventory. Re-validating prices and coupons inside the atomic block guarantees that the customer pays exactly what the system confirms at the moment of purchase.
4. Session-Based Wishlist with Auth Merge
Anonymous users should be able to curate lists without accounts. When authentication occurs, session-tracked items merge into the user's permanent profile without duplicating entries.
// frontend/wishlist-merge.ts
interface WishlistItem {
productId: string;
priceSnapshot: number;
addedAt: string;
}
export async function mergeSessionWishlist(userId: string): Promise<void> {
const sessionItems: WishlistItem[] = JSON.parse(localStorage.getItem('anon_wishlist') || '[]');
if (sessionItems.length === 0) return;
const payload = {
userId,
items: sessionItems.map(item => ({
productId: item.productId,
priceSnapshot: item.priceSnapshot,
addedAt: item.addedAt
}))
};
await fetch('/api/wishlist/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
localStorage.removeItem('anon_wishlist');
}
Backend merge logic uses get_or_create with unique constraints to prevent duplicates, preserving the original price snapshot for historical comparison.
Why this works: Session tracking removes friction for casual browsers. Price snapshots at add-time enable "price drop" notifications later. The merge operation is idempotent, ensuring safe authentication flows regardless of network retries.
Pitfall Guide
| Pitfall | Explanation | Fix |
|---|
| N+1 Query Explosion in Catalogs | Looping over products in templates triggers individual DB calls for images, attributes, and stock. | Use prefetch_related with to_attr at the view level. Never lazy-load in templates. |
| Stale Cart Pricing at Checkout | Caching cart totals for performance causes mismatches when prices change mid-session. | Re-sync line items with live DB records inside a transaction before payment. |
| Race Conditions in Inventory Deduction | Concurrent checkout requests bypass stock checks, causing overselling. | Apply select_for_update() on product rows within an atomic block. |
| Mutable Order History Corruption | Orders reference live product models; catalog edits alter historical financial records. | Snapshot prices, SKUs, and metadata into OrderLine at purchase time. Decouple from catalog. |
| Session/Wishlist Merge Collisions | Duplicate entries appear when anonymous items merge with existing account lists. | Use database-level unique constraints and get_or_create with idempotent merge endpoints. |
| Hardcoded SEO & Routing | Adding informational pages requires new views, URLs, and template files. | Store page slugs, CKEditor content, and SEO metadata in a DynamicPage model with catch-all routing. |
| Ignoring Draft Preview Workflows | Marketing changes go live immediately, risking broken layouts or incorrect campaigns. | Implement session-scoped draft records with explicit publish toggles. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-traffic catalog with 10k+ SKUs | prefetch_related + Redis cache for rendered blocks | Eliminates N+1 queries while keeping DB load predictable | Low infrastructure cost, high query savings |
| Flash sale with concurrent checkouts | select_for_update() + queue-based order processing | Prevents overselling and database lock contention | Moderate compute cost, prevents revenue loss |
| Marketing team needs daily layout changes | DB-driven LayoutBlock + session drafts | Removes deployment bottlenecks and enables safe previews | Zero engineering overhead post-implementation |
| B2C with anonymous browsing | Session-tracked wishlist + auth merge | Captures intent without forcing account creation | Minimal storage cost, increases conversion tracking |
Configuration Template
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'commerce_db',
'OPTIONS': {
'options': '-c statement_timeout=5000'
}
}
}
# Enable row-level locking optimization
DJANGO_DB_POOL = True
# Session configuration for draft/wishlist tracking
SESSION_COOKIE_AGE = 1209600 # 14 days
SESSION_SAVE_EVERY_REQUEST = True
# Cache backend for catalog prefetch results
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'TIMEOUT': 300,
'KEY_PREFIX': 'catalog_prefetch'
}
}
Quick Start Guide
- Initialize the layout engine: Create the
LayoutBlock model with position, sort order, and JSON content fields. Run migrations and seed default sections.
- Wire prefetch queries: Replace raw
Product.objects.all() calls with the optimized queryset builder. Verify query count drops below 5 per catalog page.
- Implement transactional checkout: Wrap cart finalization in
transaction.atomic(). Add select_for_update() to product and coupon rows. Test concurrent requests with a load simulator.
- Deploy session wishlist: Add localStorage tracking for anonymous users. Build the merge endpoint with idempotent
get_or_create logic. Verify deduplication on login.
- Validate draft previews: Store temporary layout changes in the session. Render draft blocks when a session key matches. Publish only after explicit admin confirmation.
This architecture transforms e-commerce from a fragile, template-bound system into a resilient, data-driven platform. By enforcing transactional boundaries, optimizing catalog queries, and decoupling presentation from composition, teams gain marketing agility without sacrificing financial accuracy or checkout reliability.