Core Concepts

This page explains the foundational concepts that underpin drf-commons. A firm understanding of these concepts is recommended before working with the more advanced features of the library.

The Composable Mixin Pattern

Every drf-commons component is a mixin class. Mixins in Python are classes designed to provide specific behavior that can be combined with other classes through multiple inheritance. They do not stand alone — they are always mixed into a class that inherits from a concrete base.

This pattern is used at every layer:

Model layer:

# BaseModelMixin is itself a composition of four mixins
class BaseModelMixin(JsonModelMixin, UserActionMixin, TimeStampMixin, SoftDeleteMixin, models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

View layer:

# BaseViewSet is a composition of five action mixins
class BaseViewSet(
    CreateModelMixin,
    ListModelMixin,
    RetrieveModelMixin,
    UpdateModelMixin,
    DestroyModelMixin,
    FileExportMixin,
    GenericViewSet,
):
    pass

The benefit is that a developer can inspect any ViewSet class’s MRO and immediately understand its complete capability set, without reading documentation.

Standardized Response Envelopes

All drf-commons responses follow a consistent JSON structure. This is a contract between the API server and its clients.

Success response:

{
  "success": true,
  "timestamp": "2026-01-15T10:30:00.000000Z",
  "message": "Operation completed successfully.",
  "data": { ... }
}

Error response:

{
  "success": false,
  "timestamp": "2026-01-15T10:30:00.000000Z",
  "message": "Validation failed.",
  "errors": {
    "title": ["This field may not be blank."]
  },
  "data": null
}

The timestamp field is always an ISO 8601 UTC string, enabling deterministic client-side parsing without timezone ambiguity.

Context-Local User Resolution

The current_user subsystem uses Python’s contextvars.ContextVar to make the authenticated request user available anywhere in the call stack without explicitly passing it as a function argument.

This is particularly valuable at the model layer, where UserActionMixin auto-populates created_by and updated_by on every save() call. Without this mechanism, every serializer would need to extract the user from self.context['request'] and pass it into the validated data.

The lifecycle:

  1. CurrentUserMiddleware receives a request

  2. Calls _set_current_user(request.user), storing the user in ContextVar

  3. Returns a reset token

  4. The request handler executes (model saves, service calls, etc.)

  5. Model’s save() calls get_current_authenticated_user()

  6. CurrentUserMiddleware calls _reset_current_user(token) in finally

The use of reset tokens (rather than simply calling clear()) ensures correct behavior when middleware is nested or when context is inherited by spawned coroutines.

Soft Deletion

Soft deletion is the pattern of marking a record as deleted rather than issuing a DELETE SQL statement. The record remains in the database and can be restored. SoftDeleteMixin implements this with two fields:

  • is_active — boolean flag; True means the record is live

  • deleted_at — timestamp of when the soft delete occurred

instance.soft_delete()   # sets is_active=False, deleted_at=now()
instance.restore()       # sets is_active=True, deleted_at=None
instance.is_deleted      # property; returns not is_active

The application convention is to always filter querysets with filter(is_active=True) in viewsets. drf-commons does not apply this filter automatically — it is the developer’s responsibility to define the correct queryset scope.

class ArticleViewSet(BaseViewSet):
    queryset = Article.objects.filter(is_active=True)  # correct

Optimistic Locking

VersionMixin implements optimistic locking. Every record carries a version integer. When two concurrent processes attempt to modify the same record:

  • Process A reads version 5, modifies, attempts to write version 6

  • Process B reads version 5, modifies, attempts to write version 6

  • Process A commits: UPDATE ... WHERE version=5 succeeds, version becomes 6

  • Process B commits: UPDATE ... WHERE version=5 fails (version is now 6)

  • Process B receives VersionConflictError

This is safer than pessimistic locking (SELECT FOR UPDATE) for APIs where the “read-then-write” window spans multiple HTTP requests.

Bulk Operation Modes

Bulk update in drf-commons supports two execution modes, controlled by the use_save_on_bulk_update attribute on the ViewSet or Serializer:

Default mode (use_save_on_bulk_update = False):

Uses Django’s QuerySet.bulk_update(). This issues a single UPDATE statement covering all modified fields for all records. It is significantly more efficient for large batches but does not trigger Django signals (pre_save, post_save) or custom save() logic.

Audit fields (updated_at, updated_by) are automatically populated by BulkUpdateListSerializer when not present in the incoming payload.

Save mode (use_save_on_bulk_update = True):

Calls instance.save() for each object in the batch within a single transaction. Triggers all signals and custom save() overrides. Use this when downstream signal handlers or save() side effects are required.

Configurable Serializer Fields

The configurable field system addresses the fundamental tension in DRF serializer fields between write-time and read-time representations.

A foreign key relation to User may need to:

  • Accept a user ID on write, return full user data on read

  • Accept a username string on write, return the user ID on read

  • Be completely read-only, returning nested data

Each of these combinations is a separate field class in drf-commons, named using the {InputFormat}To{OutputFormat}Field convention:

# IdToDataField: write by ID, read as nested serializer output
author = IdToDataField(queryset=User.objects.all(), serializer=UserSerializer)

# IdOnlyField: write by ID, read as ID
category_id = IdOnlyField(queryset=Category.objects.all())

# ReadOnlyDataField: no write, read as nested serializer output
last_editor = ReadOnlyDataField(serializer=UserSerializer)

The field system is built on a ConfigurableRelatedFieldMixin that implements the input/output transformation protocol. All 20+ field types are concrete implementations of this mixin.

Settings Namespace

drf-commons reads configuration from the COMMON key in Django’s settings. The CommonSettings class resolves each setting with a defined default, falling back to the default if the key is absent from settings.COMMON.

This design ensures the library works correctly with zero configuration while allowing operators to override specific settings:

# settings.py — only override what you need
COMMON = {
    "BULK_OPERATION_BATCH_SIZE": 2000,
}
# All other settings use their defaults

The COMMON namespace prevents collision with other Django third-party package settings.