Fair Allocation with Priority

Django REST API that takes meal ratios and a total portion count, then distributes portions fairly across meal types.

Problem

Given a dictionary of {meal_id: ratio} and a total number of portions, return a JSON response showing how many portions each meal type receives. The allocation must:

  1. Guarantee every meal type gets at least a minimum share.
  2. Distribute remaining portions to the highest-ratio meal types.

Approach

A two-pass rules engine:

  1. Minimum allocation. Calculate the smallest equal portion that satisfies all meal types: ceil((1 / sum_of_ratios) * total_portions). Assign this to every meal type with a non-zero ratio. Track the remainder.

  2. Remainder to top ratios. Sort meal types by ratio descending. Distribute leftover portions equally among the tied-for-highest ratio meals, repeating until nothing remains.

This greedy approach ensures fairness first (everyone gets a baseline), then rewards priority (high-ratio meals get the surplus). The same logic applies to resource allocation in ML — distributing GPU hours or batch slots across training jobs with different priorities.

Implementation

Product model:

from django.db import models

class Product(models.Model):
    id = models.IntegerField(primary_key=True)
    name = models.CharField(max_length=100)

Serializers:

class ProductSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Product
        fields = ["id", "name"]

class PortionSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    name = serializers.CharField(max_length=120)
    portion = serializers.IntegerField()

Rules engine:

from collections import OrderedDict
from math import ceil

def run_rules(inp):
    meal_ratios, portions = inp

    if not meal_ratios:
        return meal_ratios

    portions_allocated, remainder = rule_allocate_minimum_ratio(meal_ratios, portions)

    if remainder > 0:
        portions_allocated = rule_allocate_remainder_to_high_ratios(
            meal_ratios, portions_allocated, remainder
        )
    return portions_allocated


def rule_allocate_minimum_ratio(meal_ratios, portions):
    min_to_allocate = min_allocation(meal_ratios, portions)
    meal_counts = {}
    remaining = portions

    for meal_id in meal_ratios:
        allocation = min_to_allocate if remaining > min_to_allocate else remaining
        if meal_ratios[meal_id] != 0:
            meal_counts[meal_id] = allocation
            remaining -= min_to_allocate
        else:
            meal_counts[meal_id] = 0
    return meal_counts, remaining


def rule_allocate_remainder_to_high_ratios(meal_ratios, allocated, remainder):
    top_meals = find_top_remainder_meals(meal_ratios)
    min_alloc = min_allocation(top_meals, remainder)
    if min_alloc == 0:
        min_alloc = 1
    remaining = remainder

    while remaining > 0:
        for meal_id in top_meals:
            if remaining <= 0:
                break
            alloc = min_alloc if remaining >= min_alloc else remaining
            allocated[meal_id] += alloc
            remaining -= alloc
    return allocated


def min_allocation(meal_ratios, portions):
    return ceil((1 / sum(meal_ratios.values())) * portions)


def find_top_remainder_meals(meal_ratios):
    sorted_meals = OrderedDict(
        sorted(meal_ratios.items(), key=lambda x: x[1], reverse=True)
    )
    max_val = sorted_meals[list(sorted_meals)[0]]
    return {k: v for k, v in sorted_meals.items() if v == max_val}

Views:

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all().order_by("id")
    serializer_class = ProductSerializer

class PortionDistribution(viewsets.ViewSet):
    parser_classes = (JSONParser,)

    def create(self, request, format=None):
        distribution = request.data.get("distribution")
        portions = distribution.pop("portions")
        distribution = {int(k): int(v) for k, v in distribution.items()}

        distributed_portions = response_value(distribution, portions)
        serializer = PortionSerializer(distributed_portions, many=True)
        return Response(serializer.data)

Takeaway

The two-pass approach -- guarantee minimums, then distribute surplus by priority -- is simple enough to reason about in production and general enough to apply anywhere you're dividing a fixed budget across competing consumers.


Back to Microservices & APIs