I Built an Interactive Rate Limiter Simulator to Compare Five System Design Algorithms

Rate limiting sounds straightforward:

Count the requests, define a threshold and reject anything above it.

But what happens when traffic arrives at the exact boundary between two time windows? How should a system handle legitimate bursts? Is perfect accuracy worth the additional memory cost? And how do you prevent race conditions when several servers are updating the same counters concurrently?

These questions quickly turn a seemingly simple component into a genuinely interesting distributed systems problem.

While studying rate limiter design, I decided not to stop at diagrams and theoretical explanations. I implemented the five most commonly discussed rate limiting algorithms and built an interactive simulator that runs all of them against the same traffic stream.

The goal is simple: make their trade-offs visible.

🚦 Try the live simulator:
https://rate-limiter.mhayk.workers.dev/

💻 Explore the complete implementation on GitHub:
https://github.com/mhayk/system-design/tree/main/designs/02-rate-limiter


Why Rate Limiting Matters

A rate limiter controls how frequently a client, user, device or service can perform an operation within a given period.

For example:

  • A user may create no more than two posts per second.
  • An IP address may attempt to log in only five times per minute.
  • A client may call an expensive third-party API no more than 1,000 times per day.
  • An entire platform may accept a maximum of 100,000 requests per second.

Rate limiting helps systems:

  • protect services from abusive or accidental traffic;
  • prevent downstream services from becoming overloaded;
  • control infrastructure and third-party API costs;
  • enforce fair usage across customers;
  • maintain predictable system behaviour during traffic spikes.

However, there is no single perfect rate limiting algorithm.

Each approach makes a different compromise between accuracy, memory consumption, latency, burst tolerance and implementation complexity.

That is exactly what this project demonstrates.


Five Algorithms, One Traffic Stream

The simulator implements five algorithms:

  1. Token Bucket
  2. Leaking Bucket
  3. Fixed Window Counter
  4. Sliding Window Log
  5. Sliding Window Counter

Instead of testing each algorithm with a different example, the simulator feeds the same traffic into all five.

This makes it possible to see where their behaviour begins to diverge.

A steady stream of requests can make every algorithm appear correct. The differences become clear only when the traffic becomes irregular, bursty or deliberately hostile.


1. Token Bucket

The Token Bucket algorithm maintains a bucket containing a limited number of tokens.

Tokens are added at a configured refill rate, up to the maximum capacity of the bucket. Every request consumes one token.

If a token is available, the request is accepted. If the bucket is empty, the request is rejected.

Why it is useful

Token Bucket allows short bursts while still enforcing a sustainable average request rate.

Imagine that a client has been inactive for several seconds. During that quiet period, its bucket may become full. When the client suddenly sends several requests together, it can use the stored tokens.

This is often desirable for public APIs, where a single page load may legitimately trigger multiple requests at once.

Main trade-off

The algorithm requires two parameters:

  • bucket capacity;
  • token refill rate.

Those parameters must be tuned carefully. A bucket that is too large may allow excessive bursts, while a bucket that is too small may reject perfectly valid usage.

For many general-purpose APIs, Token Bucket is a strong default choice.


2. Leaking Bucket

The Leaking Bucket algorithm behaves more like a queue.

Incoming requests are placed into a bucket with a limited capacity. Requests then leave the bucket at a constant, predictable rate.

If the queue is already full, new requests are rejected.

Why it is useful

Leaking Bucket smooths irregular traffic into a stable output rate.

This can be valuable when the downstream system has a hard processing limit, such as:

  • a legacy platform;
  • a payment provider with a strict transactions-per-second limit;
  • a mainframe;
  • a hardware device;
  • a slow external integration.

Regardless of how bursty the incoming traffic becomes, the downstream service receives work at a controlled pace.

Main trade-off

Old requests can occupy the queue for a significant amount of time.

A large burst may fill the bucket, forcing newer and potentially more relevant requests to wait or be rejected. For interactive APIs, this delay can produce a poor user experience.

Unlike most rate limiters, Leaking Bucket often defers work rather than immediately refusing it.


3. Fixed Window Counter

Fixed Window Counter divides time into predefined windows.

For example, a rule allowing five requests per minute might create windows such as:

  • 14:00:00 to 14:00:59;
  • 14:01:00 to 14:01:59;
  • 14:02:00 to 14:02:59.

Each window has its own counter. Once the counter reaches the configured limit, additional requests are rejected until the next window begins.

Why it is useful

Fixed Window Counter is:

  • easy to understand;
  • inexpensive to store;
  • straightforward to implement with Redis using operations such as INCR and EXPIRE.

It also works well when the boundary itself has business meaning, such as a daily quota that resets at midnight.

The boundary problem

This is where the simulator becomes particularly useful.

Suppose the limit is five requests per minute.

A client sends five requests at the end of one window and another five immediately after the next window begins.

Both counters remain within their individual limits. However, the system has allowed ten requests within a rolling sixty-second period.

That is twice the intended limit.

The simulator includes a Boundary Attack scenario that reproduces this behaviour visually. It is one of the clearest demonstrations of why an algorithm that appears correct under normal traffic may fail under adversarial traffic.


4. Sliding Window Log

Sliding Window Log solves the fixed window boundary problem by storing the timestamp of every request.

Whenever a new request arrives, the limiter:

  1. removes timestamps outside the current rolling window;
  2. adds the new request timestamp;
  3. counts the remaining timestamps;
  4. accepts or rejects the request based on that count.

Redis sorted sets are particularly well suited to this approach because timestamps can be used as scores.

Why it is useful

Sliding Window Log is highly accurate.

For any rolling window, the number of accepted requests can be kept within the configured threshold.

This makes it suitable for sensitive, lower-volume operations such as:

  • login attempts;
  • password resets;
  • payment initiation;
  • account recovery;
  • costly third-party API operations.

Main trade-off

Accuracy has a memory cost.

The limiter must store individual request timestamps. Memory consumption therefore grows with request volume rather than merely with the number of users.

For a high-traffic public endpoint, storing millions of timestamps can become extremely expensive.

Sliding Window Log is often the most accurate algorithm in the comparison, but not necessarily the most practical one at scale.


5. Sliding Window Counter

Sliding Window Counter combines ideas from Fixed Window Counter and Sliding Window Log.

Instead of storing every timestamp, it keeps counters for the current and previous windows.

It then estimates the traffic inside the rolling window by applying a weight to the previous window.

A simplified calculation looks like this:

estimated requests =
    current window requests
    + previous window requests × previous-window overlap

For example, if the current rolling window overlaps 70% of the previous fixed window, 70% of the previous counter contributes to the estimate.

Why it is useful

Sliding Window Counter provides:

  • smoother behaviour around window boundaries;
  • significantly lower memory consumption than a timestamp log;
  • better accuracy than a basic fixed window;
  • only a small amount of state per client and rule.

Main trade-off

The result is an approximation.

The calculation assumes that requests in the previous window were reasonably evenly distributed. In reality, they may all have arrived during a small burst.

Even so, this algorithm often provides one of the best practical compromises between accuracy and efficiency.


The Simulator: Where the Algorithms Disagree

Reading about the algorithms is useful, but watching them process the same requests makes their trade-offs much easier to understand.

The simulator includes traffic patterns designed to expose different behaviours.

Boundary Attack

Requests are placed immediately before and after a fixed window boundary.

The Fixed Window Counter may allow twice the intended rolling-window limit, while the sliding approaches behave differently.

Flash Sale Burst

A quiet service suddenly receives a large spike in requests.

Token Bucket can use tokens accumulated during the quiet period, while stricter algorithms may reject much of the burst.

Leaking Bucket Starvation

A burst fills the queue with older requests.

Newer traffic must wait, sometimes for a considerable period, or is rejected because the queue remains full.

Steady Traffic

Requests arrive at a consistent rate below the limit.

Almost every algorithm performs well.

This scenario teaches an important lesson: the happy path is not enough to evaluate a rate limiter.

Poisson Traffic

Requests have the same average rate as a steady client, but arrive in a more realistic, irregular pattern.

The algorithms begin to disagree even though the average traffic remains below the configured quota.

This reveals the difference between controlling an average rate and enforcing a hard limit within every possible rolling window.


A Deterministic Virtual Clock

The simulator does not depend on real time or use artificial delays.

Time is represented by a deterministic virtual clock. Each algorithm receives a timestamp and decides whether the request should be accepted.

As a result:

  • a two-minute traffic trace can run almost instantly;
  • every scenario produces the same result on every execution;
  • automated tests can reproduce specific examples precisely;
  • internal algorithm state can be inspected request by request;
  • race conditions can be demonstrated as controlled event interleavings.

This was an important implementation decision.

A learning simulator must be repeatable. If the output changes because of operating-system scheduling or real-time delays, it becomes far more difficult to understand why an algorithm behaved in a particular way.


The Implementation

The project includes both a command-line simulator and an interactive browser version.

The core simulator is implemented in TypeScript, with each algorithm kept in its own small and heavily documented file.

The repository also contains:

  • deterministic traffic generators;
  • named experiment scenarios;
  • request-by-request tracing;
  • ASCII timeline rendering;
  • rolling-window peak calculations;
  • a distributed race-condition demonstration;
  • automated tests reproducing the documented examples;
  • reference Redis Lua scripts;
  • architecture and distributed-system diagrams;
  • a browser-based visual simulator.

The command-line version can be used without installing external dependencies:

npm run sim -- --list
npm run sim -- --scenario=fixed-window-edge-burst
npm run sim -- --scenario=flash-sale-burst --trace
npm run sim -- --all
npm run sim -- --race
npm test

The visual version runs as a self-contained browser application and allows the parameters and traffic patterns to be changed interactively.


The Distributed Race Condition

A rate limiter can still be logically correct and fail under concurrency.

Consider a naive Redis implementation:

GET counter
check counter against limit
SET counter + 1

Two or more requests may read the same counter value before any of them writes the updated value.

For example:

Request A reads 3
Request B reads 3
Request A writes 4
Request B writes 4

The final counter is four, even though two requests were accepted. It should have been five.

Under heavy concurrency, this can allow a significant number of requests through the limiter without the counter ever reflecting what happened.

The project includes a deterministic race-condition simulation comparing:

  • a naive read-check-write implementation;
  • an atomic check-and-increment operation.

The correct solution is not to place a distributed lock around every request. That would protect correctness by damaging latency and throughput.

Instead, the decision should be executed atomically inside the data store, commonly through:

  • a Redis Lua script;
  • atomic Redis commands;
  • Redis sorted-set operations.

The repository includes production-shaped Lua reference implementations showing how these operations can be combined into a single atomic round trip.


From a Single Server to a Distributed Rate Limiter

A local in-memory counter may work while an application has only one instance.

Once multiple rate limiter instances exist, requests from the same client may reach different servers. If every instance maintains its own counters, none of them has a complete view of the client’s activity.

Sticky sessions might appear to solve this by repeatedly routing a client to the same instance, but they reduce flexibility and complicate scaling.

A more robust design keeps the rate limiter instances stateless and stores shared counters in a centralised system such as Redis.

This introduces further design decisions:

  • How should Redis keys be sharded?
  • How should hot keys be handled?
  • Should the limiter fail open or fail closed when Redis is unavailable?
  • How much latency can the limiter add to every request?
  • How should counters be synchronised across regions?
  • Is approximate global enforcement acceptable?
  • Should rejected requests be dropped or queued for later processing?

These are the questions that transform a coding exercise into a system design problem.


What I Learnt from Building It

The biggest lesson was that rate limiting is not simply about counting requests.

It is about selecting the behaviour that matches the system being protected.

  • Token Bucket is a strong choice when legitimate bursts should be accepted.
  • Leaking Bucket is useful when downstream traffic must remain smooth and predictable.
  • Fixed Window Counter is simple and efficient, but vulnerable at window boundaries.
  • Sliding Window Log provides excellent accuracy at a considerable memory cost.
  • Sliding Window Counter offers a practical compromise between precision and efficiency.

There is no universal winner.

The right choice depends on the endpoint, traffic shape, business requirements, expected scale and consequences of allowing or rejecting too many requests.

A mature platform may use several algorithms simultaneously for different operations.


Explore the Project

This project is part of my ongoing System Design study repository, where I am turning architectural concepts into executable experiments rather than keeping them only as diagrams and notes.

For the best learning experience:

  1. Choose one of the simulator scenarios.
  2. Predict how each algorithm will behave.
  3. Run the simulation.
  4. Compare the accepted requests, rejected requests and rolling-window peak.
  5. Inspect the trace and algorithm state.
  6. Change the parameters and repeat the experiment.

The cases where your prediction is wrong are usually the most valuable ones.

🚦 Open the interactive simulator:
https://rate-limiter.mhayk.workers.dev/

💻 Read the implementation, tests, diagrams and notes:
https://github.com/mhayk/system-design/tree/main/designs/02-rate-limiter

If you are studying System Design, preparing for technical interviews or simply interested in distributed systems, feel free to explore the repository, experiment with the scenarios and contribute your own ideas.

And if you find the project useful, consider giving the repository a star on GitHub. ⭐

HTTP Has a New Method: Meet QUERY from RFC 10008

For years, API developers have often had to make a choice that never felt entirely right:

  • use GET and place every filter in the URL;
  • or use POST to send a complex query in the request body.

The first option follows the correct semantics of a read operation, but becomes increasingly awkward as the query grows.

The second solves the size and structure problem, but communicates a different intention to clients, proxies, caches and the wider HTTP infrastructure.

In June 2026, the IETF published RFC 10008 — The HTTP QUERY Method, introducing an official solution for the space between GET and POST.

Yes, HTTP now has a method called:

QUERY

And no, it is not simply POST /search with a more elegant name.

The Problem QUERY Is Trying to Solve

Consider a simple search API:

GET /products?category=laptops&brand=example&minPrice=500&maxPrice=1500

So far, so good.

Now imagine that the search needs to support:

  • multiple groups of filters;
  • AND and OR conditions;
  • date ranges;
  • sorting by several fields;
  • pagination;
  • aggregations;
  • geographical filters;
  • dynamically selected fields;
  • nested rules.

The URL can quickly begin to look like an attempt to write a programming language using only &, %20 and a great deal of optimism.

GET /products?filter=%7B%22and%22%3A%5B%7B%22category%22...

As well as being difficult to read and maintain, very large URLs encounter practical limits in browsers, proxies, servers, gateways, firewalls and other intermediary systems.

The RFC also points out that URLs are more likely to appear in:

  • access logs;
  • browser histories;
  • bookmarks;
  • analytics tools;
  • intermediary systems.

A common solution is to replace GET with POST:

POST /products/search
Content-Type: application/json

{
  "category": "laptops",
  "price": {
    "minimum": 500,
    "maximum": 1500
  },
  "brands": ["example", "another-brand"],
  "sort": [
    {
      "field": "price",
      "direction": "asc"
    }
  ]
}

Technically, it works.

The problem is semantic.

The POST method does not communicate, by itself, that the operation is safe and can be repeated without modifying system state.

To a client or intermediary component that does not already understand the API, POST could mean almost anything:

  • create an order;
  • charge a card;
  • send a message;
  • start a process;
  • modify information;
  • or simply search for products.

This is where QUERY comes in.

How the QUERY Method Works

The same request could be represented like this:

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "category": "laptops",
  "price": {
    "minimum": 500,
    "maximum": 1500
  },
  "brands": ["example", "another-brand"],
  "sort": [
    {
      "field": "price",
      "direction": "asc"
    }
  ]
}

The query remains in the request body, just as it would with POST.

However, the method now explicitly communicates that the operation:

  • is a query;
  • is safe;
  • is idempotent;
  • can be retried automatically;
  • may have its response cached.

You can think of QUERY as having the body-carrying capabilities of POST, while retaining semantic properties similar to GET.

QUERY Does Not Mean “GET with a Body”

A natural reaction might be:

Why not simply send a body with a GET request?

Because HTTP does not define general semantics for content sent in a GET request.

Some implementations allow it, but clients, servers, proxies, caches and libraries may treat that body inconsistently or ignore it entirely.

QUERY removes that ambiguity.

Its body is not an accidental implementation detail. The content and its Content-Type are part of the query definition.

QUERY Is Safe

In HTTP terms, a safe method is one where the client does not request or expect a change to the state of the resource being queried.

That means a request such as:

QUERY /orders

should not cancel, update or create orders as part of the operation requested by the client.

This does not prevent the server from performing incidental internal actions such as:

  • writing logs;
  • collecting metrics;
  • populating caches;
  • updating operational statistics;
  • creating a temporary resource representing the result.

The important point is that the purpose of the request is to retrieve information, not modify the resource.

In that respect, QUERY belongs to the same semantic category as GET, HEAD and OPTIONS.

QUERY Is Idempotent

An idempotent operation can be repeated without producing additional intended effects beyond those caused by the first execution.

This matters particularly when a network failure occurs.

Imagine that a client sends a request but loses the connection before receiving the response. With an idempotent operation, the infrastructure can retry it more safely.

QUERY /telemetry
Content-Type: application/json

{
  "spacecraftId": "satellite-001",
  "metrics": [
    "battery.voltage",
    "payload.temperature"
  ]
}

Repeating this query should not alter the state of the satellite or initiate a new operational command.

It simply requests the result again.

This property allows HTTP clients and intermediary systems to implement automatic retries with less risk than they would have with a POST.

Content-Type Is Required

In RFC 10008, the request body is an essential part of the query.

For that reason, the server should reject a QUERY request when the Content-Type header is missing or does not match the content being sent.

A valid example:

QUERY /customers
Content-Type: application/json

{
  "country": "GB",
  "status": "active"
}

The format does not have to be JSON.

A server could accept other query formats:

Content-Type: application/sql
Content-Type: application/jsonpath
Content-Type: application/vnd.example.query+json

The RFC does not define a universal query language. It defines the HTTP method and allows each resource to determine the formats and query rules it supports.

The Accept-Query Header

The specification also introduces the Accept-Query response header.

It allows a server to advertise which query formats are accepted by a particular resource:

Accept-Query: application/json, "application/jsonpath"

A client can therefore discover that an endpoint supports QUERY and which formats it may send.

A response might look like this:

HTTP/1.1 200 OK
Allow: GET, HEAD, QUERY
Accept-Query: application/json

It is worth noting that Accept-Query uses the syntax defined by HTTP Structured Fields. Although it may look like a simple comma-separated header, it should be parsed according to the Structured Fields rules.

Error Handling

The RFC suggests suitable HTTP status codes for different classes of error.

400 Bad Request

This may be used when:

  • Content-Type is missing;
  • the body is malformed;
  • the content does not match the declared media type.
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "title": "Invalid query document",
  "detail": "The request body is not valid JSON."
}

415 Unsupported Media Type

This may be used when the resource does not support the submitted query format:

HTTP/1.1 415 Unsupported Media Type
Accept-Query: application/json

422 Unprocessable Content

This is appropriate when the format and syntax are valid, but the query cannot be processed.

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "title": "Invalid query",
  "detail": "The field 'customerRank' does not exist."
}

406 Not Acceptable

This may be returned when the server cannot produce a response in a format requested by the client through the Accept header.

QUERY Responses Can Be Cached

Responses to QUERY requests may be cached.

However, there is an important difference compared with GET.

With a GET request, the URI is one of the main elements used to construct the cache key.

With QUERY, the cache must also take into account:

  • the request body;
  • the Content-Type;
  • relevant metadata;
  • content negotiation information.

Consider these two requests:

QUERY /products
Content-Type: application/json

{
  "category": "laptops"
}
QUERY /products
Content-Type: application/json

{
  "category": "monitors"
}

Although they use the same URI, they represent different queries and must not incorrectly share the same cached response.

The cache key therefore needs to incorporate the submitted content.

This also makes caching QUERY requests more complex. An intermediary may need to read the complete request body before it can determine the correct cache key.

Cache Key Normalisation

The RFC allows caches to remove semantically irrelevant differences before generating the cache key.

For example, these two JSON documents may represent the same query:

{"status":"active","country":"GB"}
{
  "country": "GB",
  "status": "active"
}

A cache that properly understands the format could normalise them to improve efficiency.

However, this must be handled very carefully.

If the cache applies a different normalisation model from the server, two distinct queries could be treated as equivalent, causing the wrong response to be returned.

In multi-tenant systems or environments dealing with sensitive information, this type of mistake could become a serious security vulnerability.

Location and Content-Location

One of the more interesting parts of the RFC is the ability for the server to assign URIs to the query or its result.

Content-Location for the Result

The server may provide a URI representing the specific result of the query:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /query-results/abc123

The client could later retrieve that result using:

GET /query-results/abc123

This may be useful for:

  • expensive reports;
  • large datasets;
  • temporary results;
  • shareable responses;
  • analytical processing.

Location for an Equivalent Query

The server may also provide a URI representing the query itself:

HTTP/1.1 200 OK
Content-Type: application/json
Location: /queries/active-uk-customers

A later request might then be:

GET /queries/active-uk-customers

In this case, the server is indicating that the URI can repeat or represent the query without requiring the client to resend the original body.

The distinction is subtle but important:

  • Content-Location may identify the result that was produced;
  • Location may identify a resource equivalent to the executed query.

Redirects

Redirect behaviour is also defined.

For 301, 302, 307 and 308 responses, the client may repeat the QUERY request at the new location.

The historical behaviour that sometimes converts a POST into a GET following a 301 or 302 must not be applied to QUERY.

A 303 See Other response, on the other hand, indicates that the result may be retrieved with GET:

HTTP/1.1 303 See Other
Location: /reports/abc123

The client would then follow with:

GET /reports/abc123

This provides an interesting solution for queries that generate persistent or precomputed results.

Conditional Requests

The method can also use conditional request headers such as:

If-None-Match: "query-result-v42"

If the result has not changed, the server may respond with:

HTTP/1.1 304 Not Modified

This can reduce the cost of expensive analytical queries or results that change infrequently.

What About Security?

Moving query parameters from the URL into the request body may reduce accidental exposure.

URLs commonly appear in:

  • access logs;
  • browser histories;
  • analytics systems;
  • monitoring tools;
  • tracing platforms;
  • bookmarks;
  • referrer headers.

However, this does not make the content secret.

The request body may still be recorded by:

  • API gateways;
  • proxies;
  • web application firewalls;
  • observability platforms;
  • debugging tools;
  • backend applications.

HTTPS, authentication, authorisation, data redaction and sensible logging policies remain essential.

Care must also be taken when the server creates a URI representing a query or its result. Sensitive information from the original request body should not simply be copied into that URI.

QUERY and CORS

In browsers, QUERY is not included in the list of CORS-safelisted methods.

A cross-origin request will therefore require a preflight request:

OPTIONS /products HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: QUERY
Access-Control-Request-Headers: content-type

The server must explicitly allow the method:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, QUERY
Access-Control-Allow-Headers: Content-Type

For frontend applications, this introduces an additional implementation detail that must be considered.

QUERY Versus GET Versus POST

A simplified comparison looks like this:

PropertyGETQUERYPOST
SafeYesYesNot necessarily
IdempotentYesYesNot necessarily
Query in request bodyNo general semanticsYesYes
Response can be cachedYesYesWith limitations
Safe automatic retryYesYesDepends on the operation
Query identified by URIYesOptionalUsually not

QUERY does not replace GET or POST.

For small, simple queries that are naturally represented in the URL, GET remains an excellent choice:

GET /users/42

For commands and operations that modify state, POST, PUT, PATCH and DELETE continue to serve their respective purposes.

QUERY is most useful when a read operation requires input that is too complex to represent conveniently in the URI.

A Real-World Example

Imagine an observability platform that searches logs across several services:

QUERY /logs
Content-Type: application/vnd.example.log-query+json
Accept: application/json

{
  "services": [
    "payment-api",
    "order-api",
    "notification-worker"
  ],
  "period": {
    "from": "2026-06-24T08:00:00Z",
    "to": "2026-06-24T12:00:00Z"
  },
  "conditions": {
    "operator": "or",
    "rules": [
      {
        "field": "level",
        "operator": "equals",
        "value": "error"
      },
      {
        "field": "durationMs",
        "operator": "greaterThan",
        "value": 2000
      }
    ]
  },
  "groupBy": [
    "service",
    "errorCode"
  ],
  "limit": 100
}

Representing all of this in a URL would be possible, but hardly pleasant.

Using POST /logs/search would work, but it would not communicate generically that the operation is safe and idempotent.

QUERY expresses that intention precisely.

Can I Start Using It Now?

Technically, the method has been defined and officially registered.

In practice, the publication of an RFC does not mean immediate support across the entire ecosystem.

Before adopting QUERY in production, you would need to test at least:

  • browsers and HTTP clients;
  • frontend libraries;
  • backend frameworks;
  • web servers;
  • reverse proxies;
  • load balancers;
  • CDNs;
  • web application firewalls;
  • API gateways;
  • tracing tools;
  • caching systems;
  • SDK generators;
  • documentation tools;
  • observability platforms;
  • CORS policies.

Some tools may reject unknown methods. Others may allow them but fail to recognise their safety, idempotency or caching semantics.

There is also a risk that an intermediary may accept the request but fail to include the body correctly in the cache key.

That would be a far more serious issue than simply returning 405 Method Not Allowed.

For that reason, the first practical uses of QUERY are likely to appear in controlled environments where the client, server and infrastructure are managed by the same organisation.

What About OpenAPI?

Another practical consideration is support from API description tools.

Even if a server accepts QUERY, the surrounding ecosystem must be able to describe it correctly:

  • OpenAPI documents;
  • Swagger interfaces;
  • client generation;
  • schema validation;
  • mocks;
  • testing tools;
  • specification-driven gateways.

Until these tools support the method consistently, many teams will continue to use POST /search, even where QUERY would be semantically more appropriate.

Standards do not succeed purely because they are technically elegant. They need to be absorbed by the ecosystem.

Will QUERY Replace POST for Search Endpoints?

Probably not immediately.

The POST /search pattern is already deeply established. It is understood by frameworks, gateways, libraries and documentation tools.

QUERY offers better semantics, but adoption will depend on clear practical benefits:

  • safer automatic retries;
  • intermediary caching;
  • clearer expression of intent;
  • format discovery through Accept-Query;
  • greater standardisation across APIs.

For many internal APIs, simply replacing POST with QUERY without taking advantage of these properties may offer little immediate value.

On the other hand, public platforms, analytical APIs and sophisticated distributed systems may benefit significantly from the additional semantic clarity.

My View

RFC 10008 addresses a genuine problem.

Developers have been using request bodies for complex queries for years. What was missing was not a way to perform those queries, but a standardised way to communicate their intention to the rest of the HTTP ecosystem.

QUERY does not make something possible that was previously impossible.

It makes explicit something we were already doing ambiguously.

That matters because HTTP is not merely a transport mechanism between a frontend and a backend. Its semantics influence:

  • retries;
  • caches;
  • proxies;
  • security;
  • observability;
  • failure recovery;
  • interoperability.

When we choose POST /search, we know that the request is only a query. The rest of the infrastructure may not.

With QUERY, that information becomes part of the protocol itself.

It is still too early to know whether the method will be widely adopted or remain limited to specific APIs and platforms. Its success will depend less on the elegance of the RFC and more on support from browsers, frameworks, gateways, caches and documentation tools.

But the proposal makes sense.

After decades of choosing between enormous URLs and a POST that “does not actually change anything”, HTTP finally has an option designed specifically for complex queries.

Now the entire ecosystem only needs to agree to use it.

No pressure.

References

  • RFC 10008 — The HTTP QUERY Method
  • RFC 9110 — HTTP Semantics
  • RFC 9111 — HTTP Caching

HTTP ganhou um novo método: conheça o QUERY da RFC 10008

Durante anos, quem desenvolve APIs precisou tomar uma decisão que nem sempre parece correta:

  • usar GET e colocar todos os filtros na URL;
  • ou usar POST para enviar uma consulta complexa no corpo da requisição.

O primeiro segue corretamente a semântica de uma operação de leitura, mas começa a ficar desconfortável quando a consulta cresce.

O segundo resolve o problema do tamanho e da estrutura dos parâmetros, mas comunica uma intenção diferente para clientes, proxies, caches e outras partes da infraestrutura HTTP.

Em junho de 2026, a IETF publicou a RFC 10008 — The HTTP QUERY Method, propondo uma solução oficial para esse espaço entre GET e POST.

Sim, agora temos um método HTTP chamado:

QUERY

E não, ele não é apenas um POST /search com um nome mais elegante.

O problema que o QUERY tenta resolver

Considere uma API de pesquisa simples:

GET /products?category=laptops&brand=example&minPrice=500&maxPrice=1500

Até aqui, tudo bem.

Agora imagine que a pesquisa precisa suportar:

  • vários grupos de filtros;
  • condições AND e OR;
  • intervalos de datas;
  • ordenação por múltiplos campos;
  • paginação;
  • agregações;
  • filtros geográficos;
  • campos dinamicamente selecionados;
  • regras aninhadas.

A URL pode rapidamente começar a parecer uma tentativa de escrever uma linguagem de programação usando apenas &, %20 e muita esperança.

GET /products?filter=%7B%22and%22%3A%5B%7B%22category%22...

Além de difíceis de ler e manter, URLs muito grandes encontram limites práticos em browsers, proxies, servidores, gateways, firewalls e ferramentas intermediárias.

A própria RFC também destaca que URLs são mais propensas a aparecer em:

  • logs de acesso;
  • históricos;
  • bookmarks;
  • ferramentas de analytics;
  • sistemas intermediários.

Uma solução bastante comum é trocar o GET por POST:

POST /products/search
Content-Type: application/json 

{
   "category": "laptops",
   "price": {
     "minimum": 500,
     "maximum": 1500
   },
   "brands": ["example", "another-brand"],
   "sort": [
     {
       "field": "price",
       "direction": "asc"
     }
   ]
}

Tecnicamente, funciona.

O problema é semântico.

O método POST não informa, por si só, que essa operação é segura e pode ser repetida sem modificar o estado do sistema.

Para um cliente ou componente intermediário que não conhece aquela API, o POST pode representar qualquer coisa:

  • criar um pedido;
  • cobrar um cartão;
  • enviar uma mensagem;
  • iniciar um processo;
  • modificar informações;
  • ou simplesmente pesquisar produtos.

É nesse ponto que entra o QUERY.

Como funciona o método QUERY

A mesma consulta poderia ser representada assim:

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "category": "laptops",
  "price": {
    "minimum": 500,
    "maximum": 1500
  },
  "brands": ["example", "another-brand"],
  "sort": [
    {
      "field": "price",
      "direction": "asc"
    }
  ]
}

A consulta continua no corpo da requisição, como aconteceria com POST.

Entretanto, o método agora comunica explicitamente que a operação:

  • é uma consulta;
  • é segura;
  • é idempotente;
  • pode ser repetida automaticamente;
  • pode ter sua resposta armazenada em cache.

Podemos pensar no QUERY como uma operação com a capacidade de transportar conteúdo de um POST, mas com propriedades semânticas semelhantes às de um GET.

QUERY não significa “GET com body”

Uma possível reação seria:

Por que não enviar simplesmente um corpo em uma requisição GET?

Porque o HTTP não define uma semântica geral para conteúdo enviado em uma requisição GET.

Algumas implementações até permitem isso, mas clientes, servidores, proxies, caches e bibliotecas podem tratar esse corpo de maneiras inconsistentes ou até ignorá-lo.

O QUERY evita essa ambiguidade.

Seu corpo não é um detalhe acidental. O conteúdo e o respectivo Content-Type fazem parte da definição da consulta.

QUERY é seguro

No contexto HTTP, um método seguro é aquele no qual o cliente não solicita nem espera uma mudança no estado do recurso consultado.

Isso significa que uma chamada como:

QUERY /orders

não deveria cancelar, atualizar ou criar pedidos como parte da operação solicitada pelo cliente.

Isso não impede o servidor de realizar efeitos internos incidentais, como:

  • registrar logs;
  • coletar métricas;
  • preencher caches;
  • atualizar estatísticas operacionais;
  • criar um recurso temporário que represente o resultado.

O ponto importante é que o objetivo da requisição é consultar, não modificar o recurso.

Nesse aspecto, QUERY pertence à mesma categoria semântica de GET, HEAD e OPTIONS.

QUERY é idempotente

Uma operação idempotente pode ser repetida sem produzir efeitos pretendidos adicionais além daqueles causados pela primeira execução.

Isso é particularmente importante quando existe uma falha de rede.

Imagine que o cliente enviou uma requisição, mas perdeu a conexão antes de receber a resposta. Com uma operação idempotente, a infraestrutura pode tentar novamente com mais segurança.

QUERY /telemetry 
Content-Type: application/json 

{ 
   "spacecraftId": "satellite-001", 
   "metrics": [ 
     "battery.voltage", 
     "payload.temperature" 
   ] 
}

Repetir essa consulta não deveria alterar o estado do satélite nem iniciar uma nova operação operacional.

Ela apenas solicita novamente o resultado.

Essa característica permite que clientes, bibliotecas HTTP e componentes intermediários implementem retries automáticos sem o mesmo receio que teriam com um POST.

O Content-Type é obrigatório

Na RFC 10008, o corpo é parte essencial da consulta.

Por isso, o servidor deve rejeitar uma requisição QUERY quando o header Content-Type estiver ausente ou não for consistente com o conteúdo enviado.

Exemplo válido:

QUERY /customers
Content-Type: application/json

{
  "country": "GB",
  "status": "active"
}

O formato não precisa obrigatoriamente ser JSON.

Um servidor poderia aceitar outros tipos de consulta:

Content-Type: application/sql
Content-Type: application/jsonpath
Content-Type: application/vnd.example.query+json

A RFC não define uma linguagem universal de pesquisa. Ela define o método HTTP e permite que cada recurso determine os formatos e as regras da consulta que suporta.

O header Accept-Query

A especificação também introduz o header de resposta Accept-Query.

Ele permite que o servidor anuncie os formatos de consulta aceitos por determinado recurso:

Accept-Query: application/json, "application/jsonpath"

Um cliente pode, assim, descobrir que o endpoint suporta QUERY e quais formatos podem ser enviados.

Uma resposta poderia ser:

HTTP/1.1 200 OK 
Allow: GET, HEAD, QUERY 
Accept-Query: application/json

É importante observar que Accept-Query utiliza a sintaxe de HTTP Structured Fields. Apesar de visualmente lembrar outros headers separados por vírgula, sua interpretação deve seguir as regras definidas para Structured Fields.

Tratamento de erros

A RFC sugere status codes apropriados para diferentes problemas.

400 Bad Request

Pode ser utilizado quando:

  • o Content-Type está ausente;
  • o corpo está malformado;
  • o conteúdo não corresponde ao tipo declarado.
HTTP/1.1 400 Bad Request 
Content-Type: application/problem+json 
{ 
   "title": "Invalid query document", 
   "detail": "The request body is not valid JSON." 
}

415 Unsupported Media Type

Pode ser utilizado quando o recurso não suporta o formato enviado:

HTTP/1.1 415 Unsupported Media Type 
Accept-Query: application/json

422 Unprocessable Content

É apropriado quando o formato e a sintaxe são válidos, mas a consulta não pode ser processada.

HTTP/1.1 422 Unprocessable Content 
Content-Type: application/problem+json 

{ 
  "title": "Invalid query", 
  "detail": "The field 'customerRank' does not exist." 
}

406 Not Acceptable

Pode ser retornado quando o servidor não consegue produzir uma resposta no formato solicitado pelo cliente por meio do header Accept.

QUERY pode usar cache

Respostas a QUERY podem ser armazenadas em cache.

Entretanto, há uma diferença importante em relação a GET.

Em uma requisição GET, a URI é uma das principais partes utilizadas para construir a chave do cache.

No caso de QUERY, o cache também precisa considerar:

  • o corpo da requisição;
  • o Content-Type;
  • metadados relacionados;
  • informações de negociação de conteúdo.

Considere estas duas requisições:

QUERY /products 
Content-Type: application/json 

{ 
   "category": "laptops" 
}
QUERY /products 
Content-Type: application/json 

{ 
  "category": "monitors" 
}

Apesar de utilizarem a mesma URI, são consultas diferentes e não podem compartilhar incorretamente a mesma resposta.

A chave de cache precisa incorporar o conteúdo enviado.

Isso também torna o cache de QUERY mais complexo. Um intermediário precisa ler o corpo completo antes de conseguir determinar a chave apropriada.

Normalização da chave de cache

A RFC permite que caches removam diferenças semanticamente irrelevantes antes de gerar a chave.

Por exemplo, estes dois documentos JSON podem representar a mesma consulta:

{"status":"active","country":"GB"}
{ 
  "country": "GB", 
  "status": "active" 
}

Um cache que compreenda corretamente o formato poderia normalizá-los para aumentar a eficiência.

No entanto, isso precisa ser feito com muito cuidado.

Uma normalização diferente daquela utilizada pelo servidor pode fazer duas consultas distintas serem consideradas iguais, resultando no retorno de uma resposta incorreta.

Em sistemas multi-tenant ou que lidam com informações sensíveis, um erro desse tipo pode se transformar em uma vulnerabilidade séria.

Location e Content-Location

Um dos pontos mais interessantes da RFC é a possibilidade de o servidor atribuir URIs à consulta ou ao seu resultado.

Content-Location para o resultado

O servidor pode informar uma URI que representa o resultado específico da consulta:

HTTP/1.1 200 OK 
Content-Type: application/json 
Content-Location: /query-results/abc123

Posteriormente, o cliente poderia recuperar esse resultado com:

GET /query-results/abc123

Isso pode ser útil para:

  • relatórios caros;
  • grandes conjuntos de dados;
  • resultados temporários;
  • respostas compartilháveis;
  • processamento analítico.

Location para a consulta equivalente

O servidor também pode fornecer uma URI que represente a própria consulta:

HTTP/1.1 200 OK 
Content-Type: application/json 
Location: /queries/active-uk-customers

Uma chamada posterior poderia ser:

GET /queries/active-uk-customers

Nesse caso, o servidor afirma que a URI pode repetir a consulta sem que o cliente precise reenviar o corpo original.

A distinção é sutil, mas importante:

  • Content-Location pode identificar o resultado produzido;
  • Location pode identificar um recurso equivalente à consulta executada.

Redirecionamentos

O comportamento de redirecionamentos também foi definido.

Diante de respostas 301, 302, 307 ou 308, o cliente pode repetir uma requisição QUERY no novo destino.

O comportamento histórico que às vezes transforma um POST em GET após um 301 ou 302 não deve ser aplicado ao QUERY.

Já uma resposta 303 See Other indica que o resultado pode ser obtido com um GET:

HTTP/1.1 303 See Other 
Location: /reports/abc123

O cliente seguiria com:

GET /reports/abc123

Isso oferece uma solução interessante para consultas que produzem resultados persistentes ou pré-calculados.

Requisições condicionais

O método também pode utilizar headers condicionais, como:

If-None-Match: "query-result-v42"

Caso o resultado não tenha mudado, o servidor pode responder:

HTTP/1.1 304 Not Modified

Isso pode reduzir o custo de consultas analíticas caras ou resultados que mudam com pouca frequência.

E quanto à segurança?

Mover os parâmetros da URL para o corpo pode reduzir a exposição acidental das informações.

URLs frequentemente aparecem em:

  • access logs;
  • históricos;
  • analytics;
  • ferramentas de monitorização;
  • sistemas de tracing;
  • bookmarks;
  • headers de referência.

Entretanto, isso não torna o conteúdo secreto.

O corpo da requisição ainda pode ser registrado por:

  • API gateways;
  • proxies;
  • WAFs;
  • ferramentas de observabilidade;
  • sistemas de debugging;
  • aplicações backend.

HTTPS, autenticação, autorização, redacção de dados e políticas adequadas de logging continuam sendo indispensáveis.

Também é necessário tomar cuidado quando o servidor cria uma URI para representar uma consulta ou seu resultado. Informações sensíveis do corpo original não devem simplesmente ser copiadas para essa URI.

QUERY e CORS

Nos browsers, QUERY não faz parte da lista de métodos considerados seguros pelo mecanismo de CORS.

Uma chamada cross-origin precisará de preflight:

OPTIONS /products HTTP/1.1 
Origin: https://app.example.com 
Access-Control-Request-Method: QUERY 
Access-Control-Request-Headers: content-type

O servidor precisará autorizar explicitamente o método:

Access-Control-Allow-Origin: https://app.example.com 
Access-Control-Allow-Methods: GET, QUERY 
Access-Control-Allow-Headers: Content-Type

Para aplicações frontend, isso significa mais uma camada a ser considerada durante a implementação.

QUERY versus GET versus POST

Uma comparação simplificada ficaria assim:

PropriedadeGETQUERYPOST
SeguroSimSimPotencialmente não
IdempotenteSimSimPotencialmente não
Consulta no corpoSem semântica geral definidaSimSim
Resposta cacheávelSimSimCom limitações
Retry automático seguroSimSimDepende da operação
Consulta identificada pela URISimOpcionalNormalmente não

O QUERY não elimina GET nem POST.

Para consultas pequenas, simples e naturalmente representáveis na URL, GET continua sendo uma excelente escolha:

GET /users/42

Para comandos e operações que modificam estado, POST, PUT, PATCH e DELETE continuam cumprindo seus respectivos papéis.

O QUERY é mais interessante quando temos uma operação de leitura cuja entrada é complexa demais para ser representada de forma conveniente na URI.

Um exemplo no mundo real

Imagine uma plataforma de observabilidade que pesquisa logs de vários serviços:

QUERY /logs 
Content-Type: application/vnd.example.log-query+json 
Accept: application/json 

{ 
  "services": [ 
    "payment-api",
    "order-api",
    "notification-worker" ],
  "period": { 
    "from": "2026-06-24T08:00:00Z",
    "to": "2026-06-24T12:00:00Z"
  }, 
  "conditions": {
    "operator": "or",
    "rules": [
      { 
        "field": "level",
        "operator": "equals", 
        "value": "error"
      }, 
      {
        "field": "durationMs",
        "operator": "greaterThan",
        "value": 2000 
      }
    ] 
  }, 
  "groupBy": [ 
    "service",
    "errorCode"
 ], 
 "limit": 100
}

Representar tudo isso na URL seria possível, mas dificilmente seria agradável.

Usar POST /logs/search funcionaria, mas perderia a capacidade de comunicar genericamente que a operação é segura e idempotente.

O QUERY expressa precisamente essa intenção.

Posso começar a utilizá-lo agora?

Tecnicamente, o método está definido e registrado.

Na prática, uma RFC publicada não significa suporte imediato em todo o ecossistema.

Antes de adoptar QUERY em produção, seria necessário testar pelo menos:

  • browsers e clientes HTTP;
  • bibliotecas frontend;
  • frameworks backend;
  • servidores web;
  • reverse proxies;
  • load balancers;
  • CDNs;
  • WAFs;
  • API gateways;
  • ferramentas de tracing;
  • sistemas de cache;
  • geradores de SDK;
  • ferramentas de documentação;
  • plataformas de observabilidade;
  • políticas CORS.

Algumas ferramentas podem rejeitar métodos desconhecidos. Outras podem aceitá-los, mas não reconhecer suas propriedades de segurança, idempotência ou cache.

Também existe o risco de um componente intermediário permitir a requisição, mas não incluir corretamente o corpo na chave de cache.

Esse seria um problema muito mais grave do que simplesmente retornar um 405 Method Not Allowed.

Portanto, o primeiro uso do QUERY provavelmente acontecerá em ambientes controlados, nos quais clientes, servidores e infraestrutura são administrados pela mesma equipa.

E o OpenAPI?

Outro ponto prático será o suporte das ferramentas de definição de APIs.

Mesmo que um servidor aceite QUERY, o ecossistema ao redor precisa conseguir descrevê-lo corretamente:

  • documentos OpenAPI;
  • interfaces de Swagger;
  • geração de clientes;
  • validação de schemas;
  • mocks;
  • ferramentas de testes;
  • gateways baseados em especificações.

Até que essas ferramentas tenham suporte consistente, muitas equipas continuarão usando POST /search, mesmo que QUERY seja semanticamente mais adequado.

Padrões não vencem apenas por serem tecnicamente bons. Eles precisam ser absorvidos pelo ecossistema.

O QUERY substituirá o POST para pesquisas?

Provavelmente não de imediato.

O padrão POST /search já está profundamente estabelecido. É entendido por frameworks, gateways, bibliotecas e ferramentas de documentação.

O QUERY oferece uma semântica melhor, mas a migração depende de benefícios concretos:

  • retries automáticos mais seguros;
  • cache intermediário;
  • melhor descrição da intenção;
  • descoberta de formatos com Accept-Query;
  • padronização entre diferentes APIs.

Para muitas APIs internas, apenas trocar POST por QUERY sem aproveitar essas propriedades provavelmente terá pouco retorno.

Por outro lado, em plataformas públicas, APIs analíticas e sistemas distribuídos com infraestrutura sofisticada, essa clareza semântica pode ser bastante valiosa.

Minha visão

A RFC 10008 resolve um problema real.

Desenvolvedores já usam requisições com corpo para consultas complexas há anos. O que faltava não era uma maneira de fazer isso, mas uma maneira padronizada de comunicar a intenção ao restante do ecossistema HTTP.

O QUERY não aparece para tornar possível algo que era impossível.

Ele aparece para tornar explícito algo que já fazíamos de maneira ambígua.

Isso é importante porque HTTP não é apenas um transporte entre frontend e backend. Sua semântica influencia:

  • retries;
  • caches;
  • proxies;
  • segurança;
  • observabilidade;
  • recuperação de falhas;
  • interoperabilidade.

Ao escolher POST /search, nós sabemos que aquela chamada é apenas uma consulta. O restante da infraestrutura talvez não saiba.

Com QUERY, essa informação passa a fazer parte do protocolo.

Ainda é cedo para dizer se o método será amplamente adoptado ou se ficará restrito a APIs e plataformas específicas. Seu sucesso dependerá menos da elegância da RFC e mais do suporte de browsers, frameworks, gateways, caches e ferramentas de documentação.

Mas a proposta faz sentido.

Depois de décadas escolhendo entre URLs gigantes e um POST que “na verdade não altera nada”, o HTTP finalmente ganhou uma opção criada especificamente para consultas complexas.

Agora só falta o ecossistema inteiro concordar em utilizá-la.

Sem pressão.

Referências

  • RFC 10008 — The HTTP QUERY Method
  • RFC 9110 — HTTP Semantics
  • RFC 9111 — HTTP Caching

Vibe Architecture: programar com IA sem colocar a produção nas mãos do destino

Depois de quase oito anos trabalhando na mesma empresa, estou novamente à procura de uma oportunidade profissional.

Por si só, isso já seria uma mudança significativa. Mas existe um pequeno detalhe: estou voltando ao mercado numa época em que a Inteligência Artificial consegue escrever código, criar testes, explicar sistemas, encontrar bugs e, ocasionalmente, inventar uma biblioteca que nunca existiu com absoluta confiança.

Como Senior Full-Stack Developer, não consigo olhar para esse momento apenas com medo ou entusiasmo. Preciso olhar com experiência.

E é daí que surge uma ideia que tenho chamado de vibe architecture.

Antes veio o vibe coding

O vibe coding popularizou uma maneira diferente de desenvolver software: você descreve o que deseja, conversa com uma IA, aceita algumas sugestões, ajusta outras e, depois de vários prompts, alguma coisa aparece no ecrã.

Às vezes é exatamente o que você pediu.

Às vezes é um sistema de autenticação completo quando você só queria mudar a cor de um botão.

É uma abordagem extremamente poderosa para protótipos, experimentação, automação e validação rápida de ideias. A distância entre imaginar uma funcionalidade e vê-la funcionar tornou-se muito menor.

Mas existe uma diferença importante entre:

“Funcionou no meu computador.”

e:

“Pode colocar em produção, processar pagamentos e atender milhares de utilizadores.”

Normalmente, essa diferença chama-se engenharia de software.

Então, o que seria vibe architecture?

Para mim, vibe architecture é usar a velocidade da IA sem abandonar a responsabilidade técnica.

É permitir que a IA ajude a implementar, investigar, documentar e testar, enquanto o engenheiro continua responsável por perguntas como:

  • Onde cada responsabilidade deve ficar?
  • Como os componentes comunicam entre si?
  • O que acontece quando uma dependência falha?
  • Como protegemos dados sensíveis?
  • Como observamos o sistema em produção?
  • Como a aplicação será mantida daqui a dois anos?
  • Por que a IA criou seis abstrações para uma função de doze linhas?

Vibe architecture não significa desenhar algumas caixas, adicionar setas e chamar tudo de microsserviço.

Significa transformar intenção em estrutura.

A IA pode gerar uma API rapidamente. Mas alguém ainda precisa decidir se aquela API deve existir, quais limites deve respeitar, como será versionada e o que acontece quando receber cinquenta mil pedidos por minuto.

A IA pode escrever código.

Arquitetura é decidir qual código deveria ser escrito.

A experiência não perdeu valor

Existe uma narrativa recorrente de que a IA tornará desenvolvedores experientes menos necessários.

Eu vejo de outra forma.

Quanto mais código conseguimos produzir, mais importante se torna saber distinguir código útil de código apenas convincente.

Uma pessoa sem experiência pode pedir:

“Crie uma plataforma escalável para milhões de utilizadores.”

Um engenheiro experiente provavelmente perguntará:

“Quantos utilizadores temos hoje?”

Essa segunda pergunta pode economizar seis meses, três microsserviços, dois clusters Kubernetes e várias reuniões sobre custos de cloud.

Depois de anos trabalhando com frontend, backend, APIs, bancos de dados, infraestrutura, integrações, deploys e sistemas em produção, aprendi que o desafio raramente é apenas fazer uma funcionalidade funcionar.

O verdadeiro desafio é fazê-la funcionar:

  • com segurança;
  • sob carga;
  • quando uma dependência está indisponível;
  • sem destruir funcionalidades existentes;
  • com logs que realmente ajudem;
  • e de uma maneira que outro desenvolvedor consiga entender numa segunda-feira de manhã.

IA acelera a implementação. Experiência reduz as decisões erradas.

As duas coisas juntas são muito mais valiosas do que qualquer uma delas isoladamente.

O novo papel do Senior Developer

O Senior Developer da era da IA provavelmente escreverá menos código manualmente em algumas tarefas.

Isso não significa trabalhar menos.

Significa concentrar mais energia em:

  • compreender o problema real;
  • especificar requisitos com clareza;
  • definir limites arquitetónicos;
  • criar contexto para agentes de IA;
  • validar decisões técnicas;
  • revisar código gerado;
  • construir estratégias de testes;
  • avaliar segurança e privacidade;
  • controlar dívida técnica;
  • e garantir que velocidade não seja confundida com progresso.

Em outras palavras, deixamos de ser apenas autores de código e passamos também a ser orquestradores de sistemas, contexto e agentes.

O prompt torna-se parte da engenharia.

A especificação torna-se mais importante.

A arquitetura torna-se o guardrail.

E o git diff continua sendo obrigatório, porque confiança é importante, mas revisão de código também.

“Mas a IA fez tudo sozinha”

Uma demonstração de cinco minutos pode criar a impressão de que a IA desenvolveu um produto inteiro sozinha.

Normalmente, ela criou:

  • uma interface bonita;
  • algumas rotas;
  • um banco de dados;
  • autenticação;
  • e pelo menos uma chave de API exposta no frontend.

Transformar isso num produto confiável exige decisões que não aparecem no vídeo da demonstração.

É necessário pensar em autorização, rate limiting, migrações, recuperação de falhas, auditoria, acessibilidade, monitorização, custos, dependências, licenças, backups e manutenção.

O botão “Generate App” pode gerar a aplicação.

Infelizmente, ainda não existe um botão “Generate Accountability”.

Não quero competir contra a IA

Estar novamente no mercado depois de quase oito anos na mesma empresa naturalmente provoca algumas reflexões.

As ferramentas mudaram. Os processos mudaram. A velocidade mudou.

Mas não acredito que o melhor caminho seja tentar provar que consigo escrever código mais rapidamente do que uma máquina.

Seria como desafiar uma calculadora para uma competição de divisão.

Meu objetivo é mostrar que sei utilizar a IA para entregar software melhor, mais rapidamente e com responsabilidade.

Quero trabalhar em ambientes nos quais a IA não seja tratada nem como mágica, nem como ameaça, mas como uma ferramenta poderosa dentro de um processo de engenharia sólido.

Não quero ser o desenvolvedor que ignora a IA.

Também não quero ser aquele que aceita todo código gerado porque “os testes passaram” — especialmente quando os testes também foram escritos pela mesma IA.

Quero ocupar o espaço entre esses dois extremos.

Os fundamentos continuam vivos

Mesmo com agentes, modelos avançados e desenvolvimento orientado por linguagem natural, algumas coisas continuam surpreendentemente importantes:

  • requisitos claros;
  • separação de responsabilidades;
  • baixo acoplamento;
  • testes confiáveis;
  • segurança;
  • observabilidade;
  • documentação;
  • revisão;
  • simplicidade;
  • e bom senso.

A IA não elimina esses fundamentos.

Ela aumenta o impacto de aplicá-los — ou de ignorá-los.

Com ferramentas tradicionais, uma decisão arquitetónica ruim poderia levar semanas para se espalhar pelo sistema.

Com agentes de IA, podemos replicá-la em todo o repositório antes do almoço.

Minha próxima fase

Estou a iniciar uma nova etapa profissional como Senior Full-Stack Developer numa era em que desenvolver software está a ser profundamente transformado pela IA.

Levo comigo quase oito anos de contexto, entregas, incidentes, decisões difíceis, integrações, sistemas legados, deploys e aprendizagem contínua.

Também levo curiosidade.

Quero explorar desenvolvimento assistido por IA, agentes, RAG, avaliação de modelos, guardrails, automação e arquiteturas preparadas para sistemas cada vez mais inteligentes.

Mas pretendo fazer isso sem esquecer uma lição básica:

Software não precisa apenas ser gerado. Precisa ser compreendido, operado e mantido.

Talvez esse seja o verdadeiro significado de vibe architecture.

Não é permitir que a IA escolha toda a arquitetura baseada nas vibes.

É criar uma arquitetura tão clara que humanos e agentes consigam trabalhar juntos sem transformar o repositório num escape room.

A IA pode ajudar a construir o futuro do software.

Mas alguém ainda precisa revisar o pull request.

Vibe Architecture: Building with AI Without Leaving Production to Fate

After nearly eight years at the same company, I am now looking for my next professional opportunity.

That would already be a significant change on its own. But there is one small detail: I am returning to the job market at a time when Artificial Intelligence can write code, create tests, explain systems, find bugs and, occasionally, invent a library that has never existed with complete confidence.

As a Senior Full-Stack Developer, I cannot look at this moment with fear or excitement alone.

I need to look at it through the lens of experience.

And that is where an idea I have been thinking about comes in: vibe architecture.

First came vibe coding

Vibe coding popularised a different way of building software.

You describe what you want, have a conversation with an AI, accept some suggestions, reject others and, after a series of prompts, something appears on the screen.

Sometimes it is exactly what you asked for.

Sometimes it builds an entire authentication platform when all you wanted was to change the colour of a button.

It is an incredibly powerful approach for prototyping, experimentation, automation and quickly validating ideas. The distance between imagining a feature and seeing it work has become much shorter.

However, there is an important difference between:

“It works on my machine.”

and:

“We can put it into production, process payments and support thousands of users.”

That difference is usually called software engineering.

So, what is vibe architecture?

To me, vibe architecture means using the speed of AI without abandoning technical responsibility.

It means allowing AI to help with implementation, investigation, documentation and testing, while the engineer remains responsible for questions such as:

  • Where should each responsibility live?
  • How should components communicate?
  • What happens when a dependency fails?
  • How do we protect sensitive data?
  • How do we observe the system in production?
  • How will the application be maintained two years from now?
  • Why has the AI created six abstractions for a twelve-line function?

Vibe architecture does not mean drawing a few boxes, adding some arrows and calling everything a microservice.

It means turning intention into structure.

AI can generate an API very quickly. But someone still needs to decide whether that API should exist, what boundaries it should respect, how it should be versioned and what happens when it receives fifty thousand requests per minute.

AI can write code.

Architecture is deciding which code should be written.

Experience has not lost its value

There is a recurring narrative that AI will make experienced developers less necessary.

I see it differently.

The more code we can generate, the more important it becomes to distinguish useful code from code that is merely convincing.

Someone without much experience may ask:

“Build me a scalable platform for millions of users.”

An experienced engineer is more likely to ask:

“How many users do we have today?”

That second question can save six months, three microservices, two Kubernetes clusters and several meetings about cloud costs.

After years of working across frontend, backend, APIs, databases, infrastructure, integrations, deployments and production systems, I have learnt that the challenge is rarely just making a feature work.

The real challenge is making it work:

  • securely;
  • under load;
  • when a dependency is unavailable;
  • without breaking existing behaviour;
  • with logs that are genuinely useful;
  • and in a way that another developer can understand on a Monday morning.

AI accelerates implementation.

Experience reduces bad decisions.

Together, they are far more valuable than either of them on their own.

The new role of the Senior Developer

The Senior Developer in the AI era will probably write less code manually for certain tasks.

That does not mean doing less work.

It means spending more energy on:

  • understanding the real problem;
  • defining requirements clearly;
  • setting architectural boundaries;
  • creating context for AI agents;
  • validating technical decisions;
  • reviewing generated code;
  • designing testing strategies;
  • assessing security and privacy;
  • controlling technical debt;
  • and making sure speed is not confused with progress.

In other words, we are no longer only authors of code.

We are also becoming orchestrators of systems, context and agents.

The prompt becomes part of the engineering process.

The specification becomes more important.

The architecture becomes the guardrail.

And git diff remains mandatory, because trust is important, but code review is important too.

“But the AI built everything by itself”

A five-minute demonstration can create the impression that AI has built an entire product on its own.

Usually, it has built:

  • an attractive interface;
  • a few routes;
  • a database;
  • authentication;
  • and at least one API key exposed in the frontend.

Turning that into a reliable product requires decisions that do not appear in the demonstration video.

You still need to think about authorisation, rate limiting, migrations, failure recovery, auditing, accessibility, monitoring, costs, dependencies, licences, backups and maintenance.

The “Generate App” button may generate the application.

Unfortunately, there is still no “Generate Accountability” button.

I do not want to compete against AI

Returning to the job market after nearly eight years at the same company naturally brings a lot of reflection.

The tools have changed.

The processes have changed.

The speed has changed.

But I do not believe the best approach is to prove that I can write code faster than a machine.

That would be like challenging a calculator to a long division competition.

My goal is to show that I know how to use AI to deliver better software, more quickly and responsibly.

I want to work in environments where AI is treated neither as magic nor as a threat, but as a powerful tool within a solid engineering process.

I do not want to be the developer who ignores AI.

I also do not want to be the developer who accepts every generated change because “the tests passed” — especially when those tests were written by the same AI.

I want to operate somewhere between those two extremes.

The fundamentals are still alive

Even with agents, advanced models and natural-language-driven development, some things remain surprisingly important:

  • clear requirements;
  • separation of concerns;
  • low coupling;
  • reliable tests;
  • security;
  • observability;
  • documentation;
  • review;
  • simplicity;
  • and good judgement.

AI does not remove these fundamentals.

It increases the impact of applying them — or ignoring them.

With traditional tools, a poor architectural decision might take weeks to spread across a system.

With AI agents, we can replicate it throughout the entire repository before lunch.

My next chapter

I am beginning a new professional chapter as a Senior Full-Stack Developer at a time when AI is transforming how software is designed and delivered.

I bring with me nearly eight years of context, delivery, incidents, difficult decisions, integrations, legacy systems, deployments and continuous learning.

I also bring curiosity.

I want to explore AI-assisted development, agents, RAG, model evaluation, guardrails, automation and architectures designed for increasingly intelligent systems.

But I want to do so without forgetting one basic lesson:

Software does not only need to be generated. It needs to be understood, operated and maintained.

Perhaps that is the real meaning of vibe architecture.

It is not about allowing AI to choose the entire architecture based on the vibes.

It is about creating an architecture clear enough for humans and agents to work together without turning the repository into an escape room.

AI can help us build the future of software.

But someone still needs to review the pull request.

Ready for the Next Challenge

Today marks the beginning of a new chapter in my career.

After an important period in my current role, where I have had the opportunity to contribute, grow and learn a great deal, I am now starting my search for a new professional opportunity.

I move forward with genuine gratitude for everything I have experienced so far, and with great motivation for the challenges ahead — particularly in roles where I can combine my experience as a Senior Full Stack / Full Cycle Developer with my growing focus on Artificial Intelligence, product thinking and delivering real business value.

Here’s to the next chapter. 🚀

Meetup ! Awesome!

Meetup brings people together in thousands of cities to do more of what they want to do in life. It is organized around one simple idea: when we get together and do the things that matter to us, we’re at our best. And that’s what Meetup does. It brings people together to do, explore, teach and learn the things that help them come alive.

For example, people run marathons, thanks to running Meetups. They write, thanks to writing Meetups. They change their careers, thanks to career Meetups. Because at Meetups, people welcome each other. They talk, help, mentor, and support each other – all in pursuit of moving their lives forward.

My first meetup will be “Computer Science Noobs” on 27th April 2017. After, I will write my experience! Now I need to read the 5th chapter of ‘Imposter’s Handbook’ by Rob Conery, in order to make the most of the discussion.

See you later! Bye!

Minha página pessoal no ar… :-]

Iai pessoal tudo bem ?

Então, resolvi começar a publicar todas as coisas que ando estudando, pensando e desenvolvendo .. começar a compartilhar com vocês todas as coisas que tenho aprendido ao longo deste tempo, vai deste linux, C, C++, algoritmos, Qt, QML, matemática, análise assintótica, tributação do ICMS, PIS, COFINS, regimes tributários brasileiros(simples nacional, lucro presumido e lucro real), e inúmeras outras coisas que forem aparecendo, afinal acredito que todos os dias precisamos chegar ao fim dele com alguma coisa nova apreendida. O cérebro precisa sempre estar ocupado com alguma coisa e de preferência que seja uma coisa muito boa! 🙂