Road Relic Rental – Car Rental & Leasing Web Application

Homepage

Road Relic Rental 🚗

Introduction

Road Relic Rental is a frontend car rental and leasing website developed using HTML, CSS, Bootstrap, and JavaScript.

The project is designed with a modern and responsive user interface that allows users to explore cars, view rental details, and experience a simulated booking system easily.

Technologies Used

  • HTML5
  • CSS3
  • Bootstrap 5
  • JavaScript

Tags

webdev #javascript #bootstrap #html #css #frontend #projects

Features

🚘 Car Rental Interface

Users can:

  • Browse different cars
  • View car specifications
  • Check rental pricing
  • Explore leasing options

📱 Responsive Design

The website is fully responsive and optimized for:

  • Mobile devices
  • Tablets
  • Desktop systems

🎨 Modern UI Design

Implemented components include:

  • Bootstrap navigation bar
  • Carousel/slideshow
  • Booking forms
  • Cards and buttons
  • Interactive layouts

🔐 Authentication Simulation

Added:

  • Face authentication simulation
  • Fingerprint verification simulation

⭐ Review & Rating System

Users can:

  • Give ratings
  • Submit reviews and feedback

📍 GPS Tracking Simulation

Integrated a GPS tracking concept for improving the user experience in rental management.

Cars Included

  • Honda Civic
  • Lexus ES 300h
  • Toyota RAV4
  • Toyota Fortuner
  • Audi A4
  • Ford Models

Learning Outcome

This project helped me improve my understanding of:

  • Frontend development
  • Responsive web design
  • Bootstrap components
  • JavaScript DOM manipulation
  • UI/UX design concepts

It also helped me gain practical experience in building real-world styled web applications.

GitHub Repository

GitHub Link:
https://github.com/anusha-seelam/Road-Relic-Rental

Project Screenshots

Homepage

Working Page

Booking Page

Conclusion

Road Relic Rental is a user-friendly frontend web application that demonstrates modern UI/UX concepts for car rental and leasing systems.

This project was a great learning experience and helped me strengthen my frontend development skills.

Thank you for reading 🚀

GitHub Repository

🔗 GitHub Repository:
https://github.com/anusha-seelam/Road-Relic-Rental

Feedback and suggestions are always welcome 😊

Forcez Claude Code à vous contredire : 14 règles, install en 1 commande

L’enseignement de 60 jours, le ROI en trois axes

Trente-deux jours de production en solo sur un ERP, 118 808 lignes de TypeScript, six versions de doctrine, quatre relecteurs externes intégrés. J’ai compilé ce que j’ai appris en quatorze règles opérationnelles, installables en une commande : le Counterpart Toolkit v0.4.1. C’est à la fois l’enseignement matériel de soixante jours de codage solo avec Claude Code, et la cartographie des quatorze failure modes silencieux que j’ai vu se répéter — pour qui code seul avec une IA en production et n’a plus de PR review pour attraper la dérive.

Le ROI est chiffré sur trois axes, mesuré sur Rembrandt :

R4 *Falsify before fix* — cinq à dix minutes de protocole en amont du fix évitent trente à quatre-vingt-dix minutes de cycle fix-puis-rollback quand la première hypothèse plausible se révèle fausse. ROI 6 à 18× par incident. Sur soixante jours, j’ai cessé de perdre une heure deux à trois fois par semaine sur des fixes qui ne fixaient rien.

R2 *Filesystem over summary* couplée à R6 *Live/Snapshot/Cache* et aux sondes drift quotidiennes — le délai médian apparition→détection d’une divergence silencieuse passe d’invisible à 35,3 jours sur 90 jours glissants. M3 recalibrée publiquement à ≤ 30 jours dans le manifesto, parce que la cible originale (≤ 7 j) était une intuition que la pratique a refusée.

Un sub-agent challenger qui produit des objections au format imposé Tool / Question / Refutation criterion. Du désaccord matériel, pas du « are you sure? » émotionnel qui pousse à réviser sans fait nouveau.

Voici comment, en 1400 mots et une commande d’install.

Le diagnostic — l’incident qui a déclenché la doctrine

Coder seul avec une IA, c’est composer deux complaisances. Celle de l’agent, sycophant par construction parce que le reinforcement learning from human feedback l’a entraîné à plaire au prompteur. Et celle du solo, self-validating par humanité, qui valide son propre travail parce qu’il ne reste plus personne pour le contester. Mises bout à bout, ces deux complaisances produisent un drift que ni l’agent ni l’humain ne signale — et qui ne se voit qu’à l’audit, longtemps après.

C’est en préparant l’audit source unique de fin avril — ADR-0024, un travail de fond sur les divergences que je remettais à plus tard depuis trois mois — que je suis tombé sur l’écart, par hasard, en croisant deux requêtes que personne n’avait jamais croisées avant. Une fiche élève, initiale Y.B. : la colonne contacts.montant_total portait 1 159 € saisis à la main quelque part en 2024, jamais touchés depuis. La somme réelle des échéances, calculée à la volée, en faisait 2 262 €. Mille euros d’écart, sur une seule fiche, sans qu’aucune alarme n’ait jamais sonné. J’élargis le grep : cinq cent soixante contacts dans le même état, parfois à plusieurs milliers d’euros près. Et pourtant montant_total était lue chaque jour dans le dashboard trésorerie — une valeur dérivable qu’on stockait sans rafraîchisseur, traitée comme un fait passé immuable alors qu’elle aurait dû vivre à la volée. C’est exactement le piège que R6 Live/Snapshot/Cache veut empêcher, et R6 est sortie de ce moment-là.

R4 Falsify before fix, la seule règle exposée ici

Le toolkit énonce R4 en cinq étapes textuelles. Le skill falsify-before-fix en est l’invocable instance — la version que Claude Code charge dans sa session, et qu’il ne peut pas sauter quand il s’apprête à écrire du code de fix.

name: falsify-before-fix
description: Activate this skill before writing the fix code on a bug or
  incident. Triggers on "fix", "bug", "patch", "hotfix", "workaround",
  "doesn't work", "diagnose", "hypothesis", "root cause". Enforces a
  single-sentence causal hypothesis and three material probes designed
  to refute it before any line of fix code is committed.
  Operational instance of R4 of the Counterpart Toolkit.

Le protocole tient en cinq étapes : (1) formuler une hypothèse causale en une phrase (pas un symptôme — « le compteur lit depuis l’ancienne table après la migration du 12 mai » vaut mieux que « le compteur est faux ») ; (2) lister trois sondes conçues pour réfuter, pas pour confirmer, parce qu’une sonde de confirmation trouve toujours ce qu’elle cherche, par sélection ; chaque sonde porte ses trois champs Tool / Question / Refutation criterion ; (3) exécuter et reporter la sortie brute, jamais paraphrasée ; (4) brancher — aucune sonde ne réfute → on écrit le fix ; une sonde réfute → on repart d’une nouvelle hypothèse ; sondes ambiguës → quatrième sonde plus tranchante avant tout code ; (5) sortir hypothèse retenue, sondes exécutées, diff, et critère d’observation post-fix.

Pourquoi un skill et pas la règle textuelle qui vivait déjà en CLAUDE.md ? Parce que la règle textuelle n’avait pas tenu sous pression. Le 6 mai, en début d’après-midi, un bug Sentry remonte que le compteur d’inscriptions du jour affiche zéro alors qu’on en a saisi trois en matinée. Mon réflexe arrive avant le protocole, et la main est déjà sur le clavier — « le cache n’est pas invalidé », je commit, je déploie. Trente minutes plus tard, rollback : le bug est toujours là. La cause réelle, qu’une sonde grep de quatre-vingt-dix secondes aurait remontée, c’est qu’aucun appel à cache_invalidate n’existait dans le pipeline d’inscription — pas un cache obsolète, un cache absent. R4 était dans CLAUDE.md depuis trois semaines. Je ne l’ai simplement pas suivie ce jour-là, parce qu’aucun dispositif n’interrompait ma course entre le bug et le commit.

C’est la différence qui fait tout. Une règle textuelle dans CLAUDE.md, l’agent la lit au début de la session, et rien ne l’oblige à la convoquer au moment exact où il en aurait besoin. Un skill, c’est un mécanisme matériel : il se charge automatiquement sur des mots-clés (« fix », « bug », « doesn’t work ») et impose son protocole dans la session active — pas un rappel à se faire, un interrupteur que la session a déjà actionné. Dix jours après l’incident du 6 mai, le skill falsify-before-fix est commit. L’enseignement de l’échec devient un dispositif. C’est la seule des 14 règles exposée dans cet article, et les 13 autres sont dans le repo, toutes construites sur le même principe : matérialiser ce qui resterait pieux en textuel seul.

Le toolkit s’applique à lui-même, chiffres et rétractations

Six versions en 32 jours, chacune ancrée dans un fait nouveau documenté. v0.3 → v0.3.1 sur un premier relecteur externe (apparat théorique disproportionné, rétracté). v0.3.1 → v0.3.2 sur un second (sept recommandations, deux work-items ouverts). v0.3.3 instrumentation M1-M5 publiée. v0.4 séparation toolkit/manifesto sur un troisième. v0.4 → v0.4.1 sur un quatrième relecteur externe (Claude.ai web), un commit consolidé intégrant trois refactors plus la LOC mesurée.

LOC corrigé 118 808 lignes mesurées par find + wc -l sur TS/TSX/JS/JSX (exclusions explicites), contre 35 k cité dans les versions antérieures — la doctrine elle-même avait péché contre R2, Cache sans rafraîchisseur projeté sur sa propre description. M3 recalibrée ≤ 7 j → ≤ 30 j avec justification publique : la cible originale était une intuition non honorée par les 35,3 jours mesurés en pratique. M1 et M5 documentés comme échecs d’instrumentation, pas comme succès : M1 sur-sensible à 12,33 vs ≤ 1 cible, M5 classe 90 % des briefs en unknown.

Four external readers across six versions. The last two audited v0.3.3 then v0.4.1 — their objections are integrated in the release notes (manifesto §From v0.4 to v0.4.1). One reviewer called the falsify-before-fix skill “the best artifact of the doctrine among the ones I read”. v0.5 prévue 15 juillet 2026.

Steal three things in 20 minutes

Trois règles à essayer avant d’installer le reste.

R4 *Falsify before fix* — empêche le cycle fix → rollback déclenché par la première hypothèse plausible mais fausse. Détaillée ci-dessus.

R6 *Live / Snapshot / Cache* — empêche qu’une valeur dérivée stockée diverge silencieusement de sa source. Toute colonne dérivable déclare sa catégorie dans le commit qui la crée, ou le commit est rejeté.

R10 *Silent failure forbidden* — empêche que catch {}, await sans destructuration { error }, 2>/dev/null et autres mécanismes d’avalement mentent à votre observabilité jusqu’à ce que la production craque sur une dépendance en aval.

Le repo : github.com/michelfaure/doctrine-counterpart. La commande d’install :

git clone https://github.com/michelfaure/doctrine-counterpart.git && 
  cd doctrine-counterpart && 
  ./install.sh --yes /path/to/your/project

Licence CC-BY-4.0. Le manifesto promet une citation nominative dans la v0.5 pour qui propose un retour exploitable — quelle règle manque pour votre stack ? Les commentaires DEV.to sont inputs directs pour la prochaine version. R14 *spike escape hatch* couvre le code prototype destiné à disparaître sous sept jours, exempté de R6/R7/R8 : l’adoption ne force pas la même friction au spike qu’au code de production.

Coda

Un agent qui ne vous contredit pas n’est pas un counterpart, c’est une dactylo plus rapide. Ces 14 règles restaurent le désaccord — matériellement, pas mentalement. Elles ne demandent pas à l’agent d’être moins sycophant, ni au solo d’être plus vigilant ; elles posent les dispositifs (skills invocables, hooks bloquants, sub-agent challenger) qui interrompent la course productive là où la complaisance se compose. Le toolkit est la prothèse qu’il reste au solo quand le PR review a disparu et qu’il refuse de coder à l’oreille. Si une seule des 14 vous évite un cycle fix-rollback la semaine prochaine, elle s’est déjà remboursée.

Counterpart Toolkit v0.4.1, fourteen operational rules in ~200 lines, six iterations in 32 days, four external reviews integrated. Tested on 60+ days of solo ERP (118 808 lines, 65+ ADRs). Licence CC-BY-4.0 : github.com/michelfaure/doctrine-counterpart

Make Claude Code disagree with you: a 14-rule counterpart toolkit (install in 1 command)

The 60-day lesson, ROI on three axes

Thirty-two days of solo production on an ERP, 118,808 lines of TypeScript, six doctrine versions, four external reviewers integrated. I’ve compiled what I learned into fourteen operational rules, installable in one command: the Counterpart Toolkit v0.4.1. It is both the material lesson of sixty days coding solo with Claude Code, and the mapping of the fourteen silent failure modes I’ve seen repeat — for anyone coding alone with an AI in production, who no longer has a PR review to catch the drift.

ROI is quantified on three axes, measured on Rembrandt:

R4 *Falsify before fix* — five to ten minutes of upstream protocol prevent thirty to ninety minutes of fix-then-rollback cycle when the first plausible hypothesis turns out wrong. ROI 6 to 18× per incident. Over sixty days, I stopped losing an hour two to three times a week on fixes that fixed nothing.

R2 *Filesystem over summary* paired with R6 *Live/Snapshot/Cache* and the daily drift probes — median apparition→detection of a silent divergence drops from invisible to 35.3 days over a 90-day rolling window. M3 publicly recalibrated to ≤ 30 days in the manifesto, because the original target (≤ 7 days) was an intuition practice refused.

A sub-agent challenger producing objections in the imposed format Tool / Question / Refutation criterion. Material disagreement, not emotional “are you sure?” that pushes you to revise without a new fact.

Here’s how, in 1400 words and one install command.

The diagnosis — the incident that triggered the doctrine

Coding alone with an AI means compounding two complaisances. The agent’s, sycophantic by construction because reinforcement learning from human feedback trained it to please the prompter. And the solo’s, self-validating by humanity, who validates their own work because no one is left to contest it. End to end, these two complaisances produce a drift that neither agent nor human flags — and that only surfaces at audit time, long after.

It was while preparing the late-April source-of-truth audit — ADR-0024, a deep-dive on divergences I had been putting off for three months — that I stumbled onto the gap, by chance, crossing two queries no one had ever crossed before. One student record, initials Y.B.: the contacts.montant_total column carried €1,159 entered by hand somewhere in 2024, untouched since. The actual sum of instalments, computed on the fly, came to €2,262. A thousand-euro gap, on a single record, with no alarm ever ringing. I widened the grep: five hundred and sixty contacts in the same state, some off by several thousand euros. And yet montant_total was read every day in the treasury dashboard — a derivable value being stored without a refresher, treated as an immutable past fact when it should have lived on the fly. This is exactly the trap R6 Live/Snapshot/Cache is meant to prevent, and R6 came out of that moment.

R4 Falsify before fix, the one rule exposed here

The toolkit states R4 as a five-step textual protocol. The falsify-before-fix skill is its invocable instance — the version Claude Code loads into its session, and cannot skip when about to write fix code.

name: falsify-before-fix
description: Activate this skill before writing the fix code on a bug or
  incident. Triggers on "fix", "bug", "patch", "hotfix", "workaround",
  "doesn't work", "diagnose", "hypothesis", "root cause". Enforces a
  single-sentence causal hypothesis and three material probes designed
  to refute it before any line of fix code is committed.
  Operational instance of R4 of the Counterpart Toolkit.

The protocol holds in five steps: (1) formulate the hypothesis in one sentence as a cause, not a symptom (“the counter reads from the old table after the 12 May migration” beats “the counter is wrong”); (2) list three probes designed to refute, not to confirm, because a confirmation probe always finds what it’s looking for by selection; each probe carries its three fields Tool / Question / Refutation criterion; (3) execute and report raw output, never paraphrased; (4) branch — no probe refutes → write the fix; one probe refutes → restart with a new hypothesis; ambiguous probes → fourth sharper probe before any code; (5) output the retained hypothesis, the probes executed, the diff, and the post-fix observation criterion.

Why a skill and not the textual rule that already lived in CLAUDE.md? Because the textual rule didn’t hold under pressure. 6 May, mid-afternoon. A Sentry alert reports the day’s enrolment counter at zero while three enrolments were entered that morning. My reflex arrives before the protocol, and my hand is already on the keyboard — “the cache isn’t invalidated.” I commit, I deploy. Thirty minutes later, rollback: the bug is still there. The actual cause, that a ninety-second grep probe would have surfaced, is that no cache_invalidate call existed in the enrolment pipeline at all — not a stale cache, an absent one. R4 had been in CLAUDE.md for three weeks. I simply didn’t follow it that day, because no apparatus interrupted my course between bug and commit.

That’s the difference that makes everything. A textual rule in CLAUDE.md, the agent reads it at session start, and nothing forces it to summon the rule at the exact moment it would need it. A skill is something else: a material mechanism that loads automatically on keywords (“fix”, “bug”, “doesn’t work”) and imposes its protocol on the active session — not a self-reminder, a switch the session has already flipped for you. Ten days after the 6 May incident, the falsify-before-fix skill was committed. The lesson of the failure becomes an apparatus. It is the one rule of the 14 exposed in this article, and the other 13 are in the repo, all built on the same principle: materialise what would remain pious in text alone.

The toolkit applied to itself — figures and retractions

Six versions in 32 days, each anchored in a documented new fact. v0.3 → v0.3.1 on a first external reviewer (disproportionate theoretical apparatus, retracted). v0.3.1 → v0.3.2 on a second (seven recommendations, two open work-items). v0.3.3 — M1–M5 instrumentation published. v0.4 — toolkit/manifesto separation on a third reviewer. v0.4 → v0.4.1 on a fourth external reviewer (Claude.ai web), a consolidated commit integrating three refactors plus the measured LOC.

LOC corrected to 118,808 lines measured by find + wc -l on TS/TSX/JS/JSX (explicit exclusions), against the 35k figure cited in earlier versions — the doctrine itself had sinned against R2, Cache without refresher projected onto its own description. M3 recalibrated ≤ 7 d → ≤ 30 d with public justification: the original target was an intuition unhonoured by the 35.3 days measured in practice. M1 and M5 documented as instrumentation failures, not as successes: M1 over-sensitive at 12.33 vs ≤ 1 target, M5 classifies 90% of briefs as unknown.

Four external readers across six versions. The last two audited v0.3.3 then v0.4.1 — their objections are integrated in the release notes (manifesto §From v0.4 to v0.4.1). One reviewer called the falsify-before-fix skill “the best artifact of the doctrine among the ones I read”. v0.5 scheduled 15 July 2026.

Steal three things in 20 minutes

Three rules to try before installing the rest.

R4 *Falsify before fix* — prevents the fix → rollback cycle triggered by the first plausible but wrong hypothesis. Detailed above.

R6 *Live / Snapshot / Cache* — prevents a stored derived value from silently diverging from its source. Any derivable column declares its category in the commit that creates it, or the commit is rejected.

R10 *Silent failure forbidden* — prevents catch {}, await without { error } destructuring, 2>/dev/null and other swallowing mechanisms from lying to your observability until production cracks on a downstream dependency.

The repo: github.com/michelfaure/doctrine-counterpart. The install command:

git clone https://github.com/michelfaure/doctrine-counterpart.git && 
  cd doctrine-counterpart && 
  ./install.sh --yes /path/to/your/project

License CC-BY-4.0. The manifesto promises nominal citation in v0.5 for anyone who proposes actionable feedback — which rule is missing for your stack? DEV.to comments are direct inputs for the next version. R14 *spike escape hatch* covers prototype code meant to disappear within seven days, exempt from R6/R7/R8: adoption does not impose the same friction on a spike as on production code.

Coda

An agent that doesn’t disagree with you isn’t a counterpart — it’s a faster typist. These 14 rules restore the disagreement — materially, not mentally. They don’t ask the agent to be less sycophantic, nor the solo to be more vigilant; they install the apparatus (invocable skills, blocking hooks, sub-agent challenger) that interrupts the productive course where complaisance compounds. The toolkit is the prosthesis the solo has left when PR review has disappeared and they refuse to code by ear. If a single one of the 14 spares you a fix-rollback cycle next week, it has already paid for itself.

Counterpart Toolkit v0.4.1, fourteen operational rules in ~200 lines, six iterations in 32 days, four external reviews integrated. Tested on 60+ days of solo ERP (118,808 lines, 65+ ADRs). License CC-BY-4.0: github.com/michelfaure/doctrine-counterpart

RabbitMQ vs Kafka: Choosing the Right Messaging System for Real Backend Architectures (part-1)

I hadn’t planned a two-part series, but as I write it’s become clear the topic can’t be contained in a single article.

Modern backend systems are increasingly event-driven.
Order processing, payment workflows, notifications, audit pipelines, analytics, inventory updates — almost every scalable system today relies on asynchronous communication between services.

At some point, teams usually face a familiar question:

Should we use RabbitMQ or Kafka?

Most comparisons stop at feature matrices:

  • RabbitMQ is a queue
  • Kafka is a stream
  • RabbitMQ is simple
  • Kafka scales better

While technically true, those comparisons rarely help when designing real production systems.

In practice, choosing the wrong messaging platform introduces operational complexity, reliability issues, scaling bottlenecks, and failure scenarios that only become visible under load.

The more important question is not:

“Which technology is better?”

The real question is:

“Which messaging model fits the architectural problem we are solving?”

That distinction matters.
RabbitMQ and Kafka solve fundamentally different categories of problems.
Understanding that difference is far more valuable than memorizing feature comparisons.

In this article, I’ll take you to look at:

  • their core architectural models,
  • delivery and ordering guarantees,
  • scalability characteristics,
  • operational tradeoffs, and
  • where each system fits best in real backend architectures.

1. The Fundamental Architectural Difference
The biggest mistake engineers make when comparing RabbitMQ and Kafka is assuming they solve the same problem.

They do not.

At a high level:

  • RabbitMQ is designed around message delivery.
  • Kafka is designed around event storage and streaming.

That single distinction influences everything else:

  • throughput,
  • ordering,
  • retries,
  • replayability,
  • and scaling

RabbitMQ: Smart Broker for Task Distribution
RabbitMQ follows a traditional broker-centric queueing model.

Producers publish messages to an exchange.
The broker routes those messages into queues.
Consumers process messages from those queues.

Once a consumer acknowledges a message, the broker removes it.

That lifecycle makes RabbitMQ extremely effective for:

  • task distribution,
  • workflow orchestration,
  • background processing,
  • request decoupling, and
  • transactional asynchronous flows.

A typical example would be:

  • order placed,
  • generate invoice,
  • reserve inventory,
  • send email,
  • trigger shipment workflow.

In these systems, the primary concern is usually:

“Has the message been processed successfully?”

RabbitMQ optimizes heavily for that use case.

Its routing capabilities are also powerful:

  • direct exchanges,
  • topic exchanges,
  • fanout patterns,
  • dead-letter routing,
  • delayed retries,
  • priority queues.

This makes RabbitMQ particularly good at workflow-style architectures where delivery control matters more than long-term event retention.

Conceptually, RabbitMQ behaves like a highly capable delivery system.

Once the package is delivered and acknowledged, it is gone.

Kafka: Distributed Event Log
Kafka approaches messaging from a very different angle.

Kafka is fundamentally a distributed append-only log.

Messages are written sequentially into partitions and persisted for a configurable retention period, regardless of whether consumers process them immediately.

Consumers do not “own” messages.
Instead, consumers track offsets representing how far they have read from the log.

This changes the model entirely.

In Kafka:

  • messages are immutable events,
  • consumers are independent readers, and
  • replayability becomes a first-class capability.

That architecture makes Kafka extremely effective for:

  • event streaming,
  • analytics pipelines,
  • audit systems,
  • event sourcing,
  • CDC pipelines, and
  • high-throughput distributed systems.

A critical advantage of Kafka is that events remain available even after consumption.

That enables:

  • replaying failed consumers,
  • rebuilding downstream systems,
  • reprocessing historical events,
  • bootstrapping new services, and
  • maintaining durable event history.

This is why Kafka is commonly used in systems where events themselves are valuable assets.

Conceptually, Kafka behaves less like a queue and more like a distributed event database.

Consumers are simply reading from it at their own pace.

Why This Difference Matters
This architectural distinction directly affects system design.

If the problem is:

  • workflow execution,
  • job distribution,
  • retries,
  • routing complexity,
  • transactional async processing

RabbitMQ often feels more natural.

If the problem is:

  • massive event ingestion,
  • event replay,
  • stream processing,
  • analytics,
  • immutable event history

Kafka becomes significantly stronger.

Many engineering teams choose Kafka primarily because it is considered “more scalable.”

That is often the wrong abstraction.

Scalability alone should not drive architectural decisions.

Operational simplicity, delivery semantics, replay requirements, failure recovery patterns, and consumer behavior are usually far more important.

In practice, some organizations even use both:

  • RabbitMQ for transactional workflows,
  • Kafka for event streaming and analytics.

That hybrid model is often more practical than forcing one technology to solve every asynchronous problem.

2. Delivery Guarantees & Reliability
In distributed systems, failures are normal.

Networks fail.
Consumers crash.
Deployments interrupt processing.
Databases timeout.
Messages get duplicated.

This is where messaging systems become more than just transport layers.
Their delivery guarantees directly affect system reliability.

At-Most-Once Delivery
In this model, messages are delivered once at most.

If something fails before processing completes, the message may be lost.

This approach favors performance over reliability.

Most production systems avoid this model for critical workflows because silent message loss is extremely difficult to debug later.

At-Least-Once Delivery
This is the most common reliability model in real systems.

The broker guarantees that a message will eventually be delivered, but duplicates are possible.

Both RabbitMQ and Kafka primarily operate in this space.

This means:

  • messages may be retried,
  • consumers may receive duplicates,
  • applications must be designed to handle reprocessing safely.

This is where many systems fail.

The messaging platform alone cannot guarantee business correctness.

The application layer still needs:

  • idempotency,
  • safe retry handling,
  • de-duplication strategies, and
  • transactional boundaries.

For example:

  • charging a payment twice,
  • sending duplicate emails,
  • creating duplicate orders,
  • are usually application design problems, not broker problems.

The Reality of “Exactly-Once”
Kafka introduced exactly-once semantics to reduce duplication scenarios between producers and consumers.

While useful, the term is often misunderstood.

In practice, exactly-once processing across:

  • databases,
  • external APIs,
  • payment gateways,
  • email services, and
  • downstream systems
    is still extremely difficult.

The moment a workflow leaves Kafka and interacts with external systems, application-level idempotency becomes necessary again.

This is why experienced engineers rarely rely solely on messaging guarantees.

They design systems assuming:

duplicates will eventually happen.

That mindset produces far more resilient architectures.

RabbitMQ Reliability Model
RabbitMQ relies heavily on:

  • acknowledgments,
  • durable queues,
  • persistent messages, and
  • retry routing.

A message remains in the queue until acknowledged by a consumer.

If the consumer crashes before acknowledgment:

  • the message is requeued,
  • and another consumer can process it.

This works very well for:

  • transactional workflows,
  • background jobs,
  • task processing, and
  • workflow orchestration.

RabbitMQ gives fine-grained control over retries and failure routing, which is one reason it remains popular for operational workflows.

Kafka Reliability Model
Kafka approaches reliability differently.

Messages are persisted into partitions and retained independently of consumer state.

Consumers maintain offsets representing processed positions.

If a consumer crashes:

  • it resumes from the last committed offset.

This model is extremely powerful for:

  • replayability,
  • large-scale event processing,
  • recovery pipelines, and
  • distributed analytics systems.

Instead of relying on broker-side retries, Kafka often pushes retry and recovery strategies into consumer applications.

That gives flexibility, but also increases architectural responsibility.

3. Ordering Guarantees
Ordering sounds simple until systems scale.

In distributed systems, maintaining strict ordering usually comes with tradeoffs:

  • lower parallelism,
  • lower throughput, and
  • operational complexity.

This is another area where RabbitMQ and Kafka behave very differently.

RabbitMQ Ordering Behavior
RabbitMQ preserves ordering within a queue under simple consumption patterns.

But ordering becomes harder once:

  • multiple consumers are introduced,
  • retries occur,
  • messages are requeued, or
  • workloads scale horizontally.

For example:

  • Consumer A processes Message 1 slowly
  • Consumer B processes Message 2 faster

Now processing order is already different from publish order.

In many workflow systems, this is acceptable.

But in domains like:

  • financial ledgers,
  • inventory consistency,
  • sequential state transitions

ordering guarantees become far more important.

RabbitMQ can support ordered processing, but often at the cost of reduced concurrency.

Kafka Ordering Model
Kafka provides ordering guarantees at the partition level.

Messages within a single partition remain ordered.

This is one of Kafka’s strongest design characteristics.

For example:

  • all events for a specific user,
  • order, or
  • account

can be routed to the same partition using a partition key.

That ensures sequential event processing for that entity.

However, Kafka does not provide global ordering across partitions.

And global ordering at scale is expensive anyway.

Most large systems eventually shift toward:

  • partition-local ordering,
  • entity-level consistency, and
  • eventual consistency models.

That tradeoff allows Kafka to scale horizontally while preserving meaningful ordering guarantees.

The Real Engineering Tradeoff
Strict ordering and high scalability often conflict with each other.

Experienced engineers usually optimize for:

  • correctness where it matters, and
  • parallelism where it does not.

Trying to maintain global ordering across massive distributed systems often creates bottlenecks faster than expected.

4. Throughput, Scalability & Backpressure

Messaging systems are usually introduced to improve scalability.

Ironically, they can also become scaling bottlenecks themselves if designed poorly.

High throughput alone is not enough.

The real question is:

Can the system continue processing reliably under sustained load?

That is where scalability and backpressure handling become critical.

RabbitMQ Scalability Characteristics
RabbitMQ performs extremely well for moderate to high throughput transactional workloads.

It is especially effective when:

  • messages require complex routing,
  • processing logic is task-oriented, and
  • workflows need delivery guarantees.

However, RabbitMQ scaling is still broker-centric.

As message volume grows:

  • queues become larger,
  • consumers compete more aggressively,
  • memory usage increases, and
  • broker pressure becomes more visible.

Large queue buildup is often an early warning sign.

In production systems, I’ve seen queue depth silently increase for hours before downstream services eventually collapsed under retry pressure.

RabbitMQ works best when:

  • consumers keep pace with producers,
  • workloads remain operationally manageable, and
  • queue growth is monitored carefully.

Kafka Scalability Characteristics
Kafka was designed with large-scale event ingestion in mind.

Its architecture favors:

  • sequential disk writes,
  • partition-based parallelism, and
  • distributed scaling.

Instead of scaling around queues, Kafka scales around partitions.

More partitions allow:

  • higher producer throughput,
  • parallel consumer processing, and
  • better horizontal scalability.

This makes Kafka extremely effective for:

  • telemetry pipelines,
  • analytics systems,
  • clickstream processing,
  • IoT ingestion, and
  • high-volume event streaming.

Kafka can handle enormous throughput, but scaling it properly introduces operational complexity:

  • partition planning,
  • consumer rebalancing,
  • lag monitoring,
  • storage management, and
  • cluster tuning.

High throughput systems are rarely “set and forget.”

Understanding Backpressure
Backpressure happens when producers generate messages faster than consumers can process them.

Every messaging system eventually faces this problem.

In RabbitMQ:

  • queues begin growing rapidly,
  • memory usage increases,
  • retries accumulate, and
  • downstream systems become overloaded.

In Kafka:

  • consumer lag increases,
  • partitions accumulate unprocessed events, and
  • recovery time grows significantly.

Neither system magically solves slow consumers.

The real solution usually involves:

  • scaling consumers,
  • reducing processing latency,
  • controlling retries,
  • implementing rate limiting, and
  • improving downstream resilience.

One of the most dangerous assumptions in distributed systems is:

“The broker will absorb the traffic.”

Eventually, every queue becomes someone else’s production incident.

In the next-part of the article, I’d cover topics like retry handling, DLQs, replayability and operational complexity.

Appreciate your suggestions & support.

ESP32 GARDEN

Construyendo ESP32 Garden: Un Sistema IoT de Monitorización Agrícola de Extremo a Extremo

La gestión botánica eficaz requiere una monitorización ambiental precisa. Depender de suposiciones para los horarios de riego o las condiciones ambientales a menudo conduce a una salud subóptima de las plantas. Para solucionar esto, desarrollé ESP32 Garden, un sistema IoT de monitorización agrícola de extremo a extremo.

Este proyecto captura datos ambientales en tiempo real utilizando sensores de hardware, los procesa a través de una arquitectura de microservicios personalizada en Python y visualiza los datos históricos en un panel web interactivo . Además, use una carcasa impresa en 3D para proteger los microcontroladores de la exposición a la tierra y la humedad.

A continuación, se detalla la arquitectura del sistema, los componentes de hardware y el stack de software utilizados para dar vida a este proyecto.

La Arquitectura de Hardware

El sistema utiliza un microcontrolador ESP32, seleccionado principalmente por sus capacidades Wi-Fi integradas y su decente potencia de procesamiento. Se interconecta con los siguientes componentes de hardware:

  • Sensor DHT11: Captura la temperatura ambiente y la humedad relativa.
  • Sensor Analógico de Humedad del Suelo: Mide el contenido volumétrico de agua en la tierra.
  • Sensor de Lluvia / Gotas: Detecta eventos de precipitación.
  • Pantalla OLED (SSD1306): Proporciona depuración y lectura de estado en tiempo real en el propio dispositivo, sin necesidad de acceder al panel web.

Diseñando la Carcasa

Desplegar componentes electrónicos expuestos en un entorno agrícola introduce riesgos significativos de cortocircuitos debido al agua y la suciedad. Para mitigar esto,imprimí en 3D una carcasa personalizada. Diseñada para parecerse a una pequeña casa, esta cubierta aloja de forma segura el ESP32 y el cableado, dejando solo las sondas de los sensores necesarias expuestas al entorno.

El Stack de Software

La infraestructura de software es modular, dividiendo las responsabilidades entre el firmware integrado, una interfaz gráfica de gestión de escritorio, la ingesta de datos en segundo plano y la visualización frontend.

1. El Firmware: C++,Arduino

El ESP32 actúa como un dispositivo de borde (edge device) ejecutando un servidor web ligero. En lugar de renderizar una página HTML pesada en el microcontrolador, expone una API REST a través del endpoint /json. Cuando se le consulta, construye y devuelve las lecturas actuales de los sensores en un payload JSON estructurado.

void handleJSON() {
  String json = "{";
  json += ""temperatura":" + String(temperatura) + ",";
  json += ""humedad_amb":" + String(humedadAmb) + ",";
  json += ""suelo":" + String(porcSuelo) + ",";
  json += ""lluvia":" + String(porcLluvia);
  json += "}";
  server.send(200, "application/json", json);
}

2. Servicio de Ingesta de Datos

Para registrar los datos a lo largo del tiempo, un script de Python actúa como un demonio en segundo plano. Envía peticiones HTTP periódicamente a la dirección IP del ESP32, analiza los datos JSON entrantes, añade una marca de tiempo y registra la entrada en un archivo CSV local utilizando pandas.

while True:
    try:
        r = requests.get(URL, timeout=5)
        raw = r.json()

        nueva_fila = {
            'fecha_hora': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'temperatura': raw.get('temperatura', 0),
            'humedad_amb': raw.get('humedad_amb', 0),
            'suelo': raw.get('suelo', 0),
            'lluvia': raw.get('lluvia', 0)
        }

        df = pd.DataFrame([nueva_fila])

        if not os.path.exists(CSV):
            df.to_csv(CSV, index=False, columns=COLUMNAS)
        else:
            df.to_csv(CSV, mode='a', header=False, index=False, columns=COLUMNAS)

    except Exception as e:
        print(f"Error de red: {e}")

    time.sleep(5)

3. Panel de Visualización de Datos

Los datos en bruto requieren interpretación. Utilicé streamlit y plotly para desarrollar una aplicación web que lee el archivo CSV compilado y renderiza gráficos interactivos de series temporales. Esto permite un análisis detallado de tendencias, como monitorizar la tasa exacta de agotamiento de la humedad del suelo.

import streamlit as st
import plotly.express as px

# Renderizar gráficos interactivos con Plotly
col_left, col_right = st.columns(2)

with col_left:
    fig_suelo = px.area(df_filtrado, x='fecha_hora', y='suelo', 
                        title="Humedad del Suelo (%)",
                        color_discrete_sequence=['#7D4F39'])
    st.plotly_chart(fig_suelo, use_container_width=True)
    st.write(f"**Media de humedad (Suelo):** {df_filtrado['suelo'].mean():.1f}%")

with col_right:
    fig_hum_amb = px.area(df_filtrado, x='fecha_hora', y='humedad_amb', 
                          title="Humedad Ambiental (%)",
                          color_discrete_sequence=['#0077B6'])
    st.plotly_chart(fig_hum_amb, use_container_width=True)

4. Gestor de Configuración (main.py)

Para agilizar el despliegue y gestionar los microservicios, el proyecto incluye un panel de control construido con tkinter. Esta GUI permite al usuario monitorizar el estado de red del ESP32 en un hilo en segundo plano y alternar los servicios de captura de datos y del panel web mediante subprocesos (subprocesses).

def monitorear(self):
    while True:
        url = f"http://{self.ip_esp}/json"
        try:
            requests.get(url, timeout=2)
            self.lbl_status.config(text="ONLINE", fg="green")
        except:
            self.lbl_status.config(text="OFFLINE", fg="red")
        time.sleep(5)

El Montaje Final en Acción

Código Abierto y Replicación

Este proyecto acorta la brecha entre los sistemas integrados de bajo nivel y los frameworks de ciencia de datos de alto nivel. Sirve como una base altamente escalable para cualquiera que busque adentrarse en el espacio del IoT.

Todo el proyecto es de código abierto. Puedes revisar la arquitectura completa, acceder al firmware y encontrar instrucciones detalladas de configuración en mi repositorio de GitHub:

ros1ndoo/ESP32Garden en GitHub

Pasos para el Despliegue:

  1. Clona el repositorio e instala las dependencias.
  2. Conecta los sensores DHT11, de humedad del suelo y de lluvia a los pines GPIO designados del ESP32.
  3. Actualiza el archivo settup.txt con las credenciales de tu red local.
  4. Ejecuta main.py, haz clic en “Aplicar” para generar las cabeceras de C++ y flashea el ESP32.
  5. Utiliza la interfaz de Tkinter para inicializar el servicio de captura de datos y lanzar el panel de streamlit.

BigQuery, Snowflake, Redshift, Databricks, Fabric: where each one silently inflates your bill

TL;DR

  • Cloud data warehouses trap you with hidden fees: the Scan Tax (charging per terabyte scanned), the Idle Tax (60-second minimums for inactive compute), and the Complexity Tax (opaque billing units).
  • The major incumbents, BigQuery, Snowflake, Redshift, Databricks, and Fabric, force you into punishing trade-offs between bankrupting your budget on exploratory queries, eating costs for idle time, or suffering through agonizing resume latencies.
  • MotherDuck provides a modern cloud data warehouse alternative designed to eliminate these taxes with a strict 1-second billing minimum, true scale-to-zero architecture, and flat compute pricing for workloads ranging from gigabytes to petabytes with Managed DuckLake (in preview).

My worst on-call wakeup wasn’t a database melting down at 3 AM. It was an email from finance.

Someone had run a query in a BI tool, and it generated a $50,000 Google BigQuery bill overnight. It was a simple, innocent-looking query, the exact kind a junior analyst writes to explore a new dataset. But that single query triggered a full table scan on a massive, unpartitioned table, and the meter just spun and spun.

Back when we were managing our own on-prem Teradata and Oracle clusters, the pain was upfront. You paid for the hardware, the power, the cooling, and the army of DBAs needed to keep it all running. We moved to the cloud to escape that management tax, only to find a whole new set of hidden ones.

The major cloud data warehouses aren’t just selling you compute and storage. They are built on pricing models with hidden “taxes” that punish you for growing, for experimenting, and sometimes, even for being idle. Choosing a data warehouse today is like picking a commercial electricity plan. Some plans look incredibly cheap on paper but have massive “peak demand” charges that bankrupt you the moment you actually need the power.

After years of signing the checks and getting burned, I’ve decoded the pricing models of the big five: BigQuery, Snowflake, Redshift, Databricks, and Fabric. Here is exactly where the bodies are buried.

The actual storage of your data is largely a solved, commoditized problem. Across the major vendors, storage costs are cheap and highly predictable, often hovering around $23.00 per terabyte per month on-demand for Snowflake, or dropping to $0.01 per gigabyte per month for long-term storage in BigQuery. When CTOs complain about their data warehouse bills, they aren’t complaining about S3 buckets. The real financial battleground is compute, concurrency, and architecture. That’s where vendors make their margins.

The three hidden taxes designed to drain your cloud budget

Almost all surprise cloud costs stem from three specific pricing mechanics.

The Scan Tax punishes you for asking questions of your data. The Idle Tax punishes you for not running queries 24/7. The Complexity Tax (and its ugly cousin, Egress Fees) punishes you for not having a Ph.D. in vendor-specific billing models.

Vendor Pricing Unit Billing Minimum The Hidden Penalty Ideal Workload
Google BigQuery Pay-per-TB Scanned Per query Scan Tax: Unpredictable costs for ad-hoc exploration. Sporadic, well-defined queries on partitioned data.
Snowflake Per-second Credits 60 seconds Idle Tax: Pays for unused time on short queries. High-throughput BI and ETL with consistent, predictable usage patterns.
AWS Redshift Provisioned / Serverless RPUs 60 seconds / Hourly Idle & Complexity Tax: High operational overhead. Predictable, high-volume workloads with dedicated ops.
Databricks Databricks Units (DBUs) Opaque / Variable Complexity & Egress Tax: Obscured true cost. All-in-one data science and large-scale Spark ETL.
Microsoft Fabric Capacity Units (CUs) Opaque Complexity Tax: Obscured resource consumption. Enterprises fully committed to the Microsoft/Power BI ecosystem.
MotherDuck Compute-time only 1 second Predictable time-based billing; no scan or idle penalties. Modern cloud data warehouse for interactive BI to large-scale batch processing.

The scan tax: paying a penalty to analyze your own data

Google BigQuery, AWS Athena, and Azure Synapse Serverless rely heavily on a pay-per-TB-scanned model. The pitch is seductive, especially for startups: “You only pay for what you query.”

At around $5.00 to $6.25 per terabyte processed, it sounds like a bargain, until a single poorly written query costs you thousands of dollars. It’s the equivalent of going to a massive public library where you aren’t charged for the book you read, but rather a fee for every single book you had to move out of the way to find it.

This model is exactly where my $50,000 bill came from. The query was devastatingly simple:

SELECT user_id, COUNT(event_id) FROM events_log GROUP BY 1;

The problem? It lacked a WHERE clause on a partitioned date column. It triggered a full scan of a petabyte-scale table.

(For the curious: serverless scan-based engines allocate compute slots to brute-force read every underlying file block from cold storage into memory if the query planner cannot prune files via a partition key. You are paying for the massive physical I/O overhead of that distributed read, regardless of how small the final result set is.)

For truly sporadic, well-defined weekly reports, this model can be cost-effective. For interactive, exploratory BI where query patterns are unpredictable by nature, you are flying blind. It forces engineering teams to become “cost police,” constantly reviewing queries and enforcing strict partitioning schemes just to avoid financial catastrophe.

The idle tax: paying for compute you aren’t even using

I learned about the Idle Tax the hard way while building a customer-facing analytics dashboard. We initially set our provisioned data warehouse to run 24/7, but when the first bill arrived, my jaw dropped. To save money, we aggressively configured the cluster to auto-suspend after one minute of inactivity. The cost went down, but the support tickets flooded in. Our users were suffering through 10-second “resume” latencies every time they loaded a dashboard after a few minutes of quiet. We were stuck thrashing between burning budget and ruining the user experience.

Snowflake and Amazon Redshift are the clearest examples of the idle tax in practice. Their pitch is “decoupled compute and storage,” giving you production-grade scalability. You’re paying for “virtual warehouses” or “RPUs” (billed per RPU-hour) that carry a hard billing minimum, often 60 seconds.

Imagine you run 30 short, 5-second queries in an hour to power a customer-facing BI dashboard. You are not billed for 150 seconds of compute. You are billed for 30 queries * 60 seconds = 1800 seconds. You just paid for 1,650 seconds of pure idle time.

It’s like a taxi meter that charges you for a full mile even if you only drive one block.

This model is especially punishing for customer-facing embedded analytics or ad-hoc BI, where queries are spiky and short-lived. You are left with a terrible architectural choice: either over-provision a warehouse and eat the idle tax, or set it to auto-suspend aggressively and make your users suffer through long resume latencies.

For massive, 24/7 ETL workloads, a provisioned model can be highly efficient. The problem lies in applying it to intermittent workloads.

The complexity tax and egress fees: when you need a PhD to understand your bill

I once spent an entire week auditing our cloud bill only to discover that a junior engineer had accidentally scheduled a massive, daily production ETL job using Databricks’ “All-Purpose” compute instead of the purpose-built “Jobs Compute.” That single checkbox mistake silently tripled the cost of the pipeline for months. The pricing model was so opaque that nobody caught it.

This is the reality of platforms like Databricks and Microsoft Fabric. The pitch is a “unified analytics platform.” The reality is a labyrinth of proprietary billing units like DBUs (Databricks Units) or CUs (Capacity Units) that are nearly impossible to map back to actual hardware consumption.

What exactly is a DBU? The answer depends on the VM type, the cloud region, and whether it’s for an automated job or an interactive notebook. It’s like trying to buy a car and being quoted a price per spark-plug ignition. It is a direct tax on not being a platform expert.

Alongside opaque compute units, these providers often extract massive, hidden network egress charges when you try to move data out of their ecosystem, penalizing integrations and compounding the complexity tax. vendor docs pricing page

Redshift carries its own complexity tax, requiring full-time database experts to manage Workload Management (WLM) queues and cluster resizing just to keep costs in check. This operational overhead isn’t just theoretical. MotherDuck’s Mega instance at $12.00/hr is 2.2x faster and 70% cheaper than a comparable 4-node Redshift ra3.16xlarge cluster, without requiring a dedicated team to manage it.

An alternative: predictable pricing with a 1-second minimum and zero idle tax

After getting burned by all three taxes, I started looking for a warehouse built on a fairer, more transparent philosophy. That’s when I found MotherDuck. What I was really looking for was simple: a billing model I could explain to a finance team without a spreadsheet, and a cold-start fast enough that I’d never have to choose between saving money and not embarrassing myself in front of users. MotherDuck was the first warehouse where both of those were true at the same time.

True scale-to-zero with a 1-second minimum

MotherDuck has a strict 1-second minimum charge. If a query runs for 500ms, you are billed for 1 second. If it runs for 5 seconds, you are billed for 5 seconds. End-user compute (called “Ducklings”) spins up in about 100ms, so there is no painful trade-off between saving money and delivering fast performance. The 60-second minimum waste simply does not exist.

Flat compute pricing, not a scan tax

You pay a flat, hourly rate for the compute you use (e.g., ~$0.60/hr for the Pulse instance), not a penalty based on how many terabytes a query happens to touch. You can run that full table scan without fear of a five-figure bill. The cost is predictable because it is based on execution time, a metric engineers can actually reason about and optimize.

Simple, SQL-first, no DBU math

MotherDuck bills in standard compute units that map directly to vCPU and RAM. The pricing is public, flat, and easy to understand. You don’t have to deal with the JVM overhead of Spark or the convoluted cluster configurations of Databricks and Redshift. Connecting via the Python SDK takes seconds, without configuring complex IAM roles or service accounts.

Petabyte-scale without the penalty

The assumption that scale-to-zero warehouses can’t handle petabyte workloads is now outdated. MotherDuck now supports petabyte-scale workloads through Managed DuckLake (in preview), giving you the same cost-predictability and ease of use whether you are querying a few gigabytes of local CSVs or petabytes of cloud data.

Code tells the story: from expensive to predictable

To control costs on a scan-based engine, you have to rewrite the query, add a WHERE clause, and pray your tables are perfectly partitioned:

-- Still risky if a user forgets the WHERE clause
SELECT user_id, COUNT(event_id)
FROM events_log
WHERE event_date = '2026-05-18' -- Must partition and filter by this!
GROUP BY 1;

The MotherDuck equivalent: just run the query. Cost is per second of execution, not per TB scanned.

-- cost is per second of execution, not per TB scanned
SELECT user_id, COUNT(event_id)
FROM events_log
GROUP BY 1;

Putting it to the test: matching the right model to your workload

The right architecture depends entirely on your use case.

For startups to enterprise scale: You need to avoid the idle and complexity taxes at all costs. MotherDuck is designed to grow with you. With Managed DuckLake (in preview), you can scale from gigabytes to petabytes with the same simple, scale-to-zero model. It is a highly cost-effective alternative to heavy platforms like Azure Synapse.

For customer-facing embedded analytics: You need low latency, high concurrency, and strict cost controls. ClickHouse is a strong baseline here due to its raw query speed and incredible 10x storage compression, but managing ClickHouse clusters introduces significant operational overhead. MotherDuck gives you the columnar performance benefits without the management burden. Its hypertenancy model isolates compute per user, preventing “noisy neighbors.” (Each isolated user query runs inside its own secure, lightweight environment, completely decoupling one user’s compute spikes from another’s. You get the security and predictable performance of a single-tenant architecture with the cost efficiency of a multi-tenant one.) That predictable per-user cost model lets you offer more competitive and profitable pricing for your own SaaS product.

For ad-hoc BI and interactive dashboards: You are running lots of short, spiky queries. The 60-second minimum from Snowflake will destroy your budget. The 1-second minimum from MotherDuck saves you from paying for compute you never actually used.

Conclusion

Unpredictable data warehouse bills are a feature, not a bug, of the incumbents’ pricing models. They were designed in a different era, and their business models rely heavily on the waste generated by the Scan Tax, the Idle Tax, and the Complexity Tax.

The choice of a data warehouse is an architectural decision with deep financial consequences. Choose a partner whose business model supports yours, not one that profits from your idle time or accidental table scans.

After years of fighting surprise bills, I found that a simpler, more transparent model wasn’t just cheaper. It gave my team back the time we were burning on query audits and cost reviews, time we could spend building instead. That’s the real cost of a bad pricing model: not just the dollar amount on the invoice, but everything your engineers stopped doing to manage it.

Frequently Asked Questions

Which serverless warehouse minimizes idle costs?

The 60-second billing minimum is what kills budgets for intermittent workloads. Every short query, a 3-second dashboard refresh, a 7-second ad-hoc lookup, gets rounded up to a full minute. Multiply that across dozens of concurrent users and you are paying for compute that never ran. A 1-second minimum with 100ms cold-start eliminates that rounding error entirely.

Which architecture provides a better price-performance ratio for spiky, intermittent query patterns?

Provisioned systems are designed for sustained, predictable throughput. When your workload is spiky, you are paying peak rates during quiet periods and scrambling during bursts. A serverless engine that bills strictly for execution time matches your actual usage curve, so your bill tracks your activity rather than your worst-case capacity estimate.

How do I avoid the complexity tax and egress fees in cloud data warehouse pricing?

The complexity tax compounds quietly. You end up needing a FinOps specialist just to interpret the bill, let alone optimize it. The cleaner path is a platform that prices in units you can reason about without a certification: vCPU time and RAM, billed at a public flat rate. Egress fees are a separate trap. If moving data out of the platform costs money, your integration architecture is constrained by your billing model, which is backwards.

What are cost-effective alternatives to BigQuery for a small data team?

The scan model is fine when you control the query patterns. Small teams rarely do. Analysts explore, iterate, and occasionally forget partition filters. A compute-time model removes that risk entirely: a runaway query costs you the seconds it ran, not the terabytes it touched. That distinction matters enormously when you don’t have a dedicated data engineering team reviewing every query before it hits production.

How do I get predictable pricing to replace my unpredictable Snowflake costs?

Snowflake’s 60-second minimum is invisible until you do the math. If your dashboard fires 40 short queries per session and users open it 500 times a day, you are paying for hours of compute that lasted seconds. Switching to per-second billing converts that hidden multiplier into a straightforward calculation: how long did the query actually run? That’s the number on your bill.

Are there cost-effective alternatives to Azure Synapse that don’t require massive price jumps when scaling?

Synapse’s pricing tiers create awkward inflection points where crossing a usage threshold forces you into a much higher cost bracket. A flat compute model with no tier boundaries scales linearly: double the workload, roughly double the cost. Managed DuckLake extends that same model to petabyte-scale, so growth doesn’t suddenly trigger a renegotiation with your vendor.

Can a scale-to-zero serverless warehouse handle petabyte-scale data?

Scale-to-zero and petabyte-scale were mutually exclusive until recently. The assumption was that handling large data volumes required persistent, warm infrastructure. Managed DuckLake separates compute from storage cleanly enough that you can query petabytes without keeping compute running between queries. You pay for the seconds your query runs, regardless of how much data it touches.

Which data warehouse offers the most predictable pricing for embedded analytics and customer-facing dashboards?

Predictability in embedded analytics requires two things: fast cold-starts (so you aren’t paying for warm standby) and per-user compute isolation (so one customer’s heavy query doesn’t inflate everyone else’s bill). ClickHouse wins on raw speed but demands operational investment most product teams can’t justify. Hypertenancy solves the isolation problem architecturally, and a 100ms spin-up means you aren’t paying to keep compute warm between user sessions.

More OpenID Federation with pac4j and Connect2id

Before login

I strongly recommend that you read the first article about the OpenID Federation protocol.

This new article dives deeper into the OpenID Federation support in pac4j and Connect2id.

You should download and use the latest versions of both software (at least version 6.5.1 for pac4j).

1) Let’s log in (again)

a) Calling the login page

Before login

As we have previously seen, the first login generates several logs on both sides (client = RP = pac4j + server = OP = connect2id).

Several HTTP calls are required to check the JWKS and the entity statements and establish the trust chains.

This could be a performance issue if these HTTP calls were made for each login attempt, though on the second try, the logs are much less verbose before displaying the login page:

The pac4j logs:

DEBUG o.p.o.r.OidcRedirectionActionBuilder     : Request Object claim names: [iss, aud, iat, exp, jti, scope, response_type,
 redirect_uri, state, code_challenge_method, client_id, code_challenge, response_mode]
DEBUG o.p.o.r.OidcRedirectionActionBuilder     : Authz parameter names: [response_type, request, client_id, scope]
DEBUG o.p.o.r.OidcRedirectionActionBuilder     : Authentication request URL: http://127.0.0.1:8080/c2id-login
 ?response_type=code&request=eyJr...hULhwg&client_id=http%3A%2F%2Flocalhost%3A8081&scope=openid%20profile%20email

The Connect2id logs:

INFO AUTHZ-SESSION - [OP2101] Created new auth session: sid=FKEttMylh3MVBAtu59F7CXx5m4BTGXZ2_DKowRh_8eg
 client_id=http://localhost:8081 scope=[openid, profile, email]

Hopefully, trust chains have been cached depending on the expiration time and the whole plumbing has not been triggered a second time.

b) After typing in the login and password

After login

After a successful login, we get the following logs:

On the Connect2id side:

INFO SESSION-STORE - [SS0201] Added new session: sub=alice ctx=web sid_key=MhNnqmimFgizG5ALmnp-tg
INFO AUTHZ-SESSION - [OP2103] Created new consent session: sid=FKEttMylh3MVBAtu59F7CXx5m4BTGXZ2_DKowRh_8eg subject=alice
 client_id=http://localhost:8081
INFO AUTHZ-SESSION - [OP2108] Created authZ response: subject=alice client_id=http://localhost:8081 response_type=[code]
INFO TOKEN - HTTP POST request: ip=127.0.0.1 path=/c2id/token
INFO TOKEN - [OP6204] Authenticated: client_id=http://localhost:8081 method=private_key_jwt client_auth_id=NgAiEAADRhMrDiZp
INFO AUTHZ-STORE - [AS0280] Issued access token: sub=alice act=null client_id=http://localhost:8081 scope=[openid]
INFO TOKEN - [OP6225] Success response: client_id=http://localhost:8081 grant=code tokens=[access,id]
INFO USERINFO - HTTP GET request: ip=127.0.0.1 path=/c2id/userinfo
INFO AUTHZ-STORE - [AS0213] Inspected valid SELF_CONTAINED Bearer access token: sub=alice act=null client_id
 =http://localhost:8081 iat=1775213922: eyJraWQiOiJQUlJ6Iiwid...
INFO USERINFO - [OP7307] Received valid UserInfo request: sub=alice claims=null ia_id=aac191d2-dcc5-4837-8545-692e204bcc07

This is fairly obvious:

  1. A POST call is performed on the /c2id/token endpoint using the private_key_jwt client authentication method (this is what we have configured on the pac4j side)
  2. A GET call is performed on the /c2id/userinfo endpoint using the access_token as bearer (= HTTP header).

This is the regular OIDC login process even if the flow has started in a federation way.

On the pac4j side:

DEBUG o.p.o.c.e.OidcCredentialsExtractor       : Authentication response successful
DEBUG o.p.o.c.e.OidcCredentialsExtractor       : Request state: d508c0f0ad/response state: d508c0f0ad
DEBUG org.pac4j.oidc.client.OidcClient         : clean authentication attempt from session
DEBUG o.p.o.c.authenticator.OidcAuthenticator  : Token response: status=200, content={"access_token":"eyJ...bng","token_type":"Bearer","expires_in":600}
DEBUG o.p.o.c.authenticator.OidcAuthenticator  : Token response successful
DEBUG org.pac4j.oidc.client.OidcClient         : clean authentication attempt from session (second call)
DEBUG org.pac4j.oidc.client.OidcClient         : Credentials validation took: 32 ms
DEBUG org.pac4j.oidc.client.OidcClient         : credentials : OidcCredentials(code=sjtox4...NQw, accessToken=...
DEBUG org.pac4j.oidc.profile.OidcProfile       : adding => key: access_token / value: eyJh...MDB9 / class java.lang.String
DEBUG org.pac4j.oidc.profile.OidcProfile       : adding => key: expiration / value: 1775214522541 / class java.lang.Long
DEBUG org.pac4j.oidc.profile.OidcProfile       : adding => key: id_token / value: eyJ...bng / class java.lang.String
DEBUG o.p.oidc.profile.creator.TokenValidator  : Trying IDToken validator, issuer: http://127.0.0.1:8080/c2id, type: null, JWS:
DEBUG o.p.oidc.profile.creator.TokenValidator  : Validated: {"iss":"http://127.0.0.1:8080/c2id","sub":"alice",
 "aud":"http://localhost:8081","exp":1775214522,"iat":1775213922,"amr":["pwd"]}
DEBUG o.p.o.p.creator.OidcProfileCreator       : User info response: status=200, content={"sub":"alice","groups":["admin","audit"]}
DEBUG o.p.oidc.profile.OidcProfileDefinition   : converted to => key: sub / value: alice / class java.lang.String
DEBUG org.pac4j.oidc.profile.OidcProfile       : adding => key: sub / value: alice / class java.lang.String
...
DEBUG org.pac4j.oidc.profile.OidcProfile       : adding => key: token_expiration_advance / value: 0 / class java.lang.Integer
DEBUG org.pac4j.oidc.client.OidcClient         : profile: Optional[OidcProfile(super=AbstractJwtProfile(super=CommonProfile(
 super=BasicUserProfile(logger=Logger[org.pac4j.oidc.profile.OidcProfile], id=alice, attributes={access_token=eyJ...MDB9,
 token_expiration_advance=0, sub=alice, aud=[http://localhost:8081], amr=[pwd], id_token=eyJr...Hsbng,
 iss=http://127.0.0.1:8080/c2id, groups=[admin, audit], expiration=1775214522541, exp=Fri Apr 03 13:08:42 CEST 2026,

The logs are straightforward on the pac4j side as well: we see the successful authentication, the token and the userprofile calls.

c) After the login process

Even though we’re not doing anything, new logs keep appearing for the Connect2id server:

INFO AUTHZ-STORE - [AS0228] Revoking multiple authzs: client_id=http://localhost:8081
INFO CLIENT-REG - [OP5184] Deleted client: client_id=http://localhost:8081 num_revoked_authz=1
INFO FED-REG - [OP8041] Reaped 1 expired federation clients

These logs are related to the fact that we have performed an automatic registration. The logs indicate that the temporarily created client is deleted.

Indeed, as Connect2id does not know the pac4j client, it has temporarily registered this client and after some time, the registered client is cleaned.

While this is a very convenient mechanism, it can impact server performance.

Therefore, it could be useful to consider explicitly and permanently registering our OIDC pac4j client.

2) Let’s log in with explicit registration

a) The client identifier

And this is a feature supported by the OpenID Federation protocol:

the explicit registration of the OIDC client.

This must be of course supported by the OIDC server and this is the case of the Connect2id server.

pac4j supports both modes depending on the OIDC server, so the configuration must only be updated on the Connect2id server.

Stop the server (tomcat/bin/shutdown.sh), edit the tomcat/webapps/c2id/WEB-INF/oidcProvider.properties file:

op.federation.clientRegistrationTypes=explicit

and restart the server (tomcat/bin/startup.sh).

On the pac4j side:

DEBUG o.p.o.m.r.FederationClientRegister       : Registration endpoint exists and only explicit registration by OP (and RP)
 -> performing explicit registration
 INFO .f.e.DefaultEntityConfigurationGenerator : Generating entity configuration for: http://localhost:8081
DEBUG o.p.o.m.r.FederationClientRegister       : Received response registration: eyJraW...mkaMxZQ
 WARN o.p.o.m.r.FederationClientRegister       : /! ================================================
 WARN o.p.o.m.r.FederationClientRegister       : /! Explicit registration of the client 'http://localhost:8081' returns
  id: [t4j746kwjax6s]. This information won't be repeated. You MUST add this value to your configuration before the next
   application startup!
 WARN o.p.o.m.r.FederationClientRegister       : /! ================================================

On the Connect2id side:

INFO FED-REG - [OP8014] Registered entity http://localhost:8081 as explicit client with client_id=xkqolxvshcjv6 exp=1783005293
INFO FED-REG - [OP8019] Explicit registration response statement for entity http://localhost:8081: {sub=http://localhost:8081,
 aud=[http://localhost:8081], metadata={openid_relying_party={client_registration_types=[explicit, automatic],
 token_endpoint_auth_signing_alg=RS256, grant_types=[authorization_code], jwks={keys=[{kty=RSA, e=AQAB, use=sig, kid=...

The explicit registration is duly taken into account by Connect2id which generates a specific client_id for the OIDC client, returns it to pac4j to be displayed in its logs.

Let’s follow the instruction given in the logs and add this client_id in the pac4j configuration:

@Bean
public Config config() {
    final var config = new OidcConfiguration();

    // the new clientId!
    config.setClientId("xkqolxvshcjv6");

    final var rpJwks = config.getRpJwks();
    rpJwks.setJwksPath("file:./metadata/rpjwks.jwks");
    rpJwks.setKid("defaultjwks0426");
    config.setClientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
    final var privateKeyJwtConfig = new PrivateKeyJwtClientAuthnMethodConfig(rpJwks);
    config.setPrivateKeyJWTClientAuthnMethodConfig(privateKeyJwtConfig);

    config.setRequestObjectSigningAlgorithm(JWSAlgorithm.RS256);

    final var federation = config.getFederation();

    federation.setTargetOp("http://127.0.0.1:8080/c2id");
    final var trust = new OidcTrustAnchorProperties();
    trust.setIssuer("http://localhost:8081/trustanchor");
    trust.setJwksPath("classpath:trustanchor.jwks");
    federation.getTrustAnchors().add(trust);

    federation.getJwks().setJwksPath("file:./metadata/oidcfede.jwks");
    federation.getJwks().setKid("mykeyoidcfede26");
    federation.setContactName("New RP test");
    federation.setContactEmails(List.of("[email protected]"));

    federation.setEntityId("http://localhost:8081");

    return new Config(baseUri + "/callback", new OidcClient(config));
}

Here is the complete configuration for reference (and not only the added client_id).

We restart the Spring Boot application and try a new login process.

This time, no registration happens and Connect2id directly recognizes the provided client_id:

INFO AUTHZ-SESSION - [OP2101] Created new auth session: sid=reJ...58w client_id=xkqolxvshcjv6 scope=[openid, profile, email]

b) The client secret

At this point in the article, you may wonder why we only have a client_id and no client_secret.

In fact, we don’t need a secret as we use the private_key_jwt client authentication method: the credential is the private key, not the secret.

As this pac4j configuration is revealed in its entity statement, the Connect2id server is aware of that setting and, accordingly, decides to only return a client_id for this OIDC client.

Let’s go further and replace this configuration in pac4j:

config.setClientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
final var privateKeyJwtConfig = new PrivateKeyJwtClientAuthnMethodConfig(rpJwks);
config.setPrivateKeyJWTClientAuthnMethodConfig(privateKeyJwtConfig);

by:

config.setClientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);

to use the client_secret_basic authentication (and not the private_key_jwt).

We also remove the previous client_id:

config.setClientId("xkqolxvshcjv6");

and completely change the contact name:

federation.setContactName("New RP test");

Restart the Spring Boot application and try to log in.

This time, we call the Connect2id server with explicit registration and no configured client id/secret and a client_secret_basic authentication method.

And we get a new error from pac4j:

org.pac4j.oidc.exceptions.OidcException: Client secret export file is required

This seems definitely a weird one, but it’s not! Let me explain: the received client_id is output in the logs, though it would not be safe to output the client_secret in the logs as well.

So the received client_secret is planned to be saved on the disk, on a file defined by the secretExportFile property.

Let’s define it in the configuration:

federation.setSecretExportFile("./mysecret.tmp");

and try again. It works!

The generated client_id and client_secret have been added on-the-fly to the OIDC configuration and have been used to perform the client_secret_basic authentication method.

The client_id is in the logs: Explicit registration of the client 'http://localhost:8081' returns id: [wzcyln5hdtmck].

The client_secret is in the defined file: The received secret has been saved into the file: ./mysecret.tmp. Its value is: U9UaF99rUopAXnWqXqkWBj_RsOZXfM1Efs67N4KweHo.

These seem to be the right settings as the login process has worked, but we’d like to check that on the Connect2id side.

Let’s seek in the Connect2id configuration file oidcProvider.properties for the property: op.reg.apiAccessTokenSHA256. I find:

# Evaluation note: Use token value ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
op.reg.apiAccessTokenSHA256=cca68b8b82bcf0b96cb826199429e50cd95a042f8e8891d1ac56ab135d096633

Let’s try to use the Connect2id API to list the existing clients with this key:

curl -X GET http://127.0.0.1:8080/c2id/clients -H "Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6"

We get two clients:

[
    {
        "client_registration_types":[
            "explicit",
            "automatic"
        ],
        "grant_types":[
            "authorization_code"
        ],
        "jwks":{
            "keys":[
                {
                    "kty":"RSA",
                    "e":"AQAB",
                    "use":"sig",
                    "kid":"defaultjwks0426",
                    "n":"2moV...aq7Q"
                }
            ]
        },
        "subject_type":"public",
        "application_type":"web",
        "registration_client_uri":"http://127.0.0.1:8080/c2id/clients/wzcyln5hdtmck",
        "redirect_uris":[
            "http://localhost:8081/callback?client_name=OidcClient"
        ],
        "registration_access_token":"0QKFCVfBe5PqVFwgjEaRousXHKEa9My0-IMMMCTPB20.YSxpLHI",
        "token_endpoint_auth_method":"client_secret_basic",
        "client_id":"wzcyln5hdtmck",
        "client_secret_expires_at":0,
        "request_object_signing_alg":"RS256",
        "client_id_issued_at":1775836027,
        "client_secret":"U9UaF99rUopAXnWqXqkWBj_RsOZXfM1Efs67N4KweHo",
        "client_name":"New RP test",
        "contacts":[
            "[email protected]"
        ],
        "response_types":[
            "code"
        ],
        "id_token_signed_response_alg":"RS256"
    },
    {
        "token_endpoint_auth_signing_alg":"RS256",
        "grant_types":[
            "authorization_code"
        ],
        "jwks":{
            "keys":[
                {
                    "kty":"RSA",
                    "e":"AQAB",
                    "use":"sig",
                    "kid":"defaultjwks0426",
                    "n":"2moV...aq7Q"
                }
            ]
        },
        "subject_type":"public",
        "application_type":"web",
        "registration_client_uri":"http://127.0.0.1:8080/c2id/clients/xkqolxvshcjv6",
        "redirect_uris":[
            "http://localhost:8081/callback?client_name=OidcClient"
        ],
        "registration_access_token":"7ilSWZGsH4wOyehETCNJQz0My8NO-6efLiV1ED2HcN0.YSxpLHI",
        "token_endpoint_auth_method":"private_key_jwt",
        "client_id":"xkqolxvshcjv6",
        "request_object_signing_alg":"RS256",
        "client_id_issued_at":1775747499,
        "client_name":"C2ID Test RP (Localhost)",
        "contacts":[
            "[email protected]"
        ],
        "response_types":[
            "code"
        ],
        "id_token_signed_response_alg":"RS256"
    }
]

The second one has only the right client_id, no client_secret and is defined with private_key_jwt.

The first one has the right client_id and client_secret and is defined with client_secret_basic.

Notice the appropriate client_name property as well.

The Connect2id configuration perfectly matches what was received by pac4j (not that I had any doubts 😉)

The magic of the federation continues:

The pac4j RP and the Connect2id OP only know and rely on the trust anchor, they don’t know each other.

But nonetheless the RP has been able to definitely register itself on the OP!

MyNotifyAssist — A Smart Reminder & Productivity Platform 🚀

— a platform designed to help users manage reminders, routines, team coordination, notes, and daily productivity from one place.

🔥 Why I Started This Project

Many people forget:

  • Medicines
  • Important meetings
  • Daily tasks
  • Bill payments
  • Team updates
  • Routine activities

Traditional reminder apps feel limited and outdated.

So I started building a smarter solution using:

  • Java 21 + Spring Boot
  • React JS
  • React Native / Expo for mobile
  • AI-powered reminder creation (ongoing feature)

✨ Features of MyNotifyAssist

✅ Smart Reminders

Create reminders for:

  • Medicines
  • Meetings
  • Birthdays
  • Daily tasks
  • Payments
  • Custom schedules

Users can receive notifications through:

  • WhatsApp
  • SMS
  • Email
  • Push notifications

✅ To-Do Lists & Progress Tracking

Users can:

  • Create tasks
  • Track progress
  • Organize daily workflow
  • Build routines

✅ Team Collaboration

Users can:

  • Add friends or team members
  • Share reminders
  • Coordinate tasks together

Perfect for:

  • Families
  • Small teams
  • Students
  • Startups

✅ Public Notes Sharing

A unique feature I added:
Users can create notes and generate a public shareable link.

Useful for:

  • Instructions
  • Quick sharing
  • Documentation
  • Public updates

✅ Expense Splitting (Upcoming)

Currently planning features similar to Splitwise:

  • Monthly expense tracking
  • Group expense division
  • Team settlements

🤖 AI Integration (Upcoming)

One of the most exciting parts:
Users will simply type prompts like:

“Remind me tomorrow at 8 AM to take medicine”

And AI will automatically:

  • Understand the intent
  • Extract date/time
  • Schedule reminders instantly

I’m currently exploring:

  • OpenAI APIs
  • Gemini APIs
  • AI workflow integration with Spring Boot

🛠 Tech Stack

Backend:

  • Java 21
  • Spring Boot

Frontend:

  • React JS

Mobile:

  • React Native
  • Expo

Other Integrations:

  • Razorpay
  • Google Sign-In
  • Notification services
  • SMS providers

📱 Current Challenges

As an indie developer, I’m also learning:

  • Google Play deployment
  • Apple Developer setup
  • Mobile app approvals
  • Push notification systems
  • AI infrastructure
  • SEO & product growth

It’s been a huge learning experience.

🚀 Vision

I want MyNotifyAssist to become more than a reminder app.

The goal is to create:

  • A personal productivity assistant
  • AI-powered scheduling
  • Smart team coordination
  • Daily life management tools

All in one platform.

—
Join httpsz://www.MyNotifyAssist.com