class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2)
margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)
def save(self, *args, **kwargs):
# Auto-generate slug (model-specific logic)
if not self.slug:
from django.utils.text import slugify
self.slug = slugify(self.name)
# Calculate margin (business logic)
if self.price and self.cost:
self.margin = ((self.price - self.cost) / self.price) * 100
super().save(*args, **kwargs)
from django.db.models.signals import post_save
from django.dispatch import receiver
from products.models import Product
@receiver(post_save, sender=Product)
def update_inventory_cache(sender, instance, created, **kwargs):
"""Update cache when product changes (cross-app concern)."""
from django.core.cache import cache
cache.delete(f'product:{instance.id}')
@receiver(post_save, sender=Product)
def notify_price_change(sender, instance, created, **kwargs):
"""Send notification on price change (separate concern)."""
if not created:
# Check if price changed
old_product = Product.objects.get(pk=instance.pk)
if old_product.price != instance.price:
send_price_alert.delay(instance.id)
Signals and save overrides both handle model events, but have different use cases. I override save() for logic intrinsic to the model. Signals decouple logic across apps—I use them when multiple apps need to respond to model changes. Signals can make debugging harder due to implicit connections. I use post_save with created flag to distinguish create vs update. For performance-critical paths, save override is faster (no signal dispatch). I document signals clearly. Choose based on coupling needs—tight coupling favors save override, loose coupling favors signals.