{"slug": "5-design-patterns-used-in-my-new-habit-tracker-app", "title": "5 design patterns used in my new habit tracker app", "summary": "Developer Daniel Roy Greenfeld built commitgraph, a Django + HTMX habit tracker app, and shared five design patterns from its development, including pure functions for streak logic, a 7-bit integer to represent weekly schedules, and use of Python's calendar module for month grids. The patterns aim to make code testable and maintainable, with examples such as a `longest_streak` function and a `shade` helper for calendar cells.", "body_md": "# 5 design patterns used in my new habit tracker app\n\n*Shipping fast with AI but don't fully trust the code? I help developers 1:1 turn AI-built apps into something they understand and own. [How it works →](/coaching/#own-project)*\n\nThere are plenty of habit trackers, but I wanted to build mine with constraints, a calendar view and habit streaks. So I built [commitgraph](https://commitgraph.app): a small Django + HTMX app. Here are five Python design and testing patterns from building it.\n\n## 1. Pure functions\n\nStreak counting and calendar shading live in two plain modules. They take dates and return values. And work independently from Django, the database, or the user. That makes them easy to test.\n\n``` python\n# streaks.py\nfrom datetime import date, timedelta\n\ndef longest_streak(dates: list[date], is_due=lambda _: True) -> int:\n    days = set(dates)\n    if not days:\n        return 0\n    best = run = 0\n    cur, last = min(days), max(days)\n    while cur <= last:\n        if is_due(cur):\n            run = run + 1 if cur in days else 0\n            best = max(best, run)\n        cur += timedelta(days=1)\n    return best\n```\n\nThanks to this boundary the streak logic is easy to unit test; no database nor user are needed.\n\nAnother example: each calendar cell gets a shade from how much you completed that day. It is tempting to compute that in the template. Instead, I turned it into a helper function:\n\n``` php\n# calendars.py\ndef shade(completed: int, active: int) -> str:\n    if active == 0 or completed == 0:\n        return \"bg-[var(--g0)] text-muted\"\n    frac = completed / active\n    if frac >= 1:\n        return \"bg-[#a84420] text-white\"\n    if frac >= 0.6:\n        return \"bg-[#d97a4e] text-[#3a1c0e]\"\n    return \"bg-[#f6ddc9] text-[#3a1c0e]\"\n```\n\n`assert shade(5, 5).endswith(\"text-white\")` is again easy to test and it's reusable in different parts of the app.\n\n## 2. A schedule is a 7-bit integer\n\nClaude suggested this nifty approach: a habit runs on a subset of weekdays. That is seven yes/no answers, which fit cleanly into a single 7-bit integer.\n\n```\nclass Habit(models.Model):\n    ALL_DAYS = 0b1111111   # every day\n    WEEKDAYS = 0b0011111   # Mon-Fri (weekday() 0-4)\n    due_days = models.PositiveSmallIntegerField(default=ALL_DAYS)\n\n    def is_due_on(self, day: date) -> bool:\n        return bool(self.due_days & (1 << day.weekday()))\n```\n\nNo join table, no seven boolean columns. Adding \"weekends only\" is a new constant, not a migration.\n\nThe `is_due_on` one-liner treats `due_days` as a **7-bit binary calendar**, one switch per weekday, ordered right-to-left from Monday (0) to Sunday (6).\n\n`day.weekday()` gives 0-6 (Mon-Sun). `1 << day.weekday()` puts a single bit at that day's position, and `& self.due_days` is non-zero only if the habit is scheduled that day. `bool()` turns that into `True`/` False`.\n\n## 3. Let the standard library build the month grid\n\nA month grid can be tricky: leading blanks, trailing blanks, weeks that straddle two months. Python's `calendar` module already knows all of it.\n\n``` python\nimport calendar\n\nfor week in calendar.Calendar(firstweekday=0).monthdatescalendar(year, month):\n    for day in week:\n        if day.month != month:\n            ...  # a padding cell from the previous or next month\n```\n\n`monthdatescalendar` returns real `date` objects, one clean list of weeks; no need to write any logic around how many days April has.\n\n## 4. A query vocabulary on the model\n\n\"Active habits\" and \"habits active on a given day\" show up everywhere. Rather than repeating the filters in every view, I named them on a custom `QuerySet` so they are chainable:\n\n``` python\nclass HabitQuerySet(models.QuerySet):\n    def active(self):\n        return self.filter(archived_at__isnull=True)\n\n    def active_on(self, day: date):\n        return self.filter(\n            Q(start_date__lte=day)\n            & (Q(archived_at__isnull=True) | Q(archived_at__date__gte=day))\n        )\n\nclass Habit(models.Model):\n    objects = HabitQuerySet.as_manager()\n```\n\nNow `Habit.objects.filter(user=u).active()` is very readable and the relatively complex `Q` expressions are abstracted into the model.\n\n## 5. \"Done and due\" is set algebra\n\nThe Today screen needs habits that are due today and not yet checked off. Two sets and one operator.\n\n```\ndue_ids = {h.id for h in habits}\ndone_ids = set(\n    HabitCompletion.objects\n    .filter(habit__user=user, date=day)\n    .values_list(\"habit_id\", flat=True)\n) & due_ids\n```\n\nThe intersection drops any completion for a habit that is not due today, so a spurious record can't inflate the count. The logic is the math, not a nest of `if` statements.\n\nHere is a REPL snippet that demonstrates how set intersection (`&`) filters down to only the elements present in both sets:\n\n```\n>>> due_ids = {10, 11, 12, 13}      # Habits scheduled for today\n>>> completed_ids = {11, 13, 99}    # Logged completions (99 is a stray record)\n\n>>> # The '&' operator keeps ONLY IDs present in BOTH sets\n>>> done_today = completed_ids & due_ids\n>>> print(done_today)  # 10, 12 and 99 are dropped because they are not in both sets\n{11, 13}\n```\n\nI have also seen this used in RBAC logic to cross-check user roles with endpoint permissions. The intersection of the two sets is the effective permissions a user has.\n\n## Keep reading\n\n- [Build the Simplest Thing That Works](/blog/build-the-simplest-thing-that-works/)\n- [Unsubscribe links without a login: Django signing](/blog/unsubscribe-without-login-django-signing/)\n- [Learning Rust Made Me a Better Python Developer](/blog/rust-made-me-a-better-python-developer/)\n\nThe thread running through all five patterns: when a piece of logic doesn't strictly need the database or a dependency, stick to this boundary. Fetch your primitive values, do the math in plain Python, so it's more testable in isolation.\n\nWhat is one function in your Django views that would be much easier to trust and test if you decoupled it?", "url": "https://wpnews.pro/news/5-design-patterns-used-in-my-new-habit-tracker-app", "canonical_source": "https://belderbos.dev/blog/python-patterns-django-habit-tracker/", "published_at": "2026-09-06 00:00:00+00:00", "updated_at": "2026-09-07 12:57:24.436705+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["commitgraph", "Django", "HTMX", "Python"], "alternates": {"html": "https://wpnews.pro/news/5-design-patterns-used-in-my-new-habit-tracker-app", "markdown": "https://wpnews.pro/news/5-design-patterns-used-in-my-new-habit-tracker-app.md", "text": "https://wpnews.pro/news/5-design-patterns-used-in-my-new-habit-tracker-app.txt", "jsonld": "https://wpnews.pro/news/5-design-patterns-used-in-my-new-habit-tracker-app.jsonld"}}