How I would design a scalable platform using Go, Apache Kafka, Redis, Kubernetes and distributed databases to process 40 billion requests per month.

Building a platform capable of processing 40 billion requests per month might initially sound like a challenge reserved for companies such as Google, Amazon or Netflix.

However, once we convert that number into requests per second and divide the responsibilities correctly between caching, APIs, messaging systems, databases and asynchronous processing, the problem becomes much easier to understand.

In this article, I will explain how I would design a distributed architecture using Go, Apache Kafka, Redis, Kubernetes, distributed databases and modern observability tools to support this volume reliably and cost-effectively.

This is, of course, a reference architecture. The final implementation would depend on several factors, including:

  • The type of requests being processed
  • The average payload and response sizes
  • The balance between reads and writes
  • Consistency requirements
  • Geographical distribution of users
  • Data residency requirements
  • Expected availability and latency targets

Converting 40 Billion Requests into Requests per Second

Before selecting technologies, we need to understand the actual traffic volume.

Assuming a 30-day month:

40,000,000,000 requests per month
÷ 30 days
÷ 24 hours
÷ 60 minutes
÷ 60 seconds

≈ 15,432 requests per second

This gives us the following approximate traffic profile:

PeriodApproximate volume
Per month40 billion
Per day1.33 billion
Per hour55.5 million
Per minute925,000
Per second15,432

An average of approximately 15,000 requests per second is not, by itself, an extreme workload for a modern distributed platform.

The real challenge is handling traffic peaks.

Traffic is rarely distributed evenly throughout the day. Marketing campaigns, notifications, scheduled integrations or external events can multiply the normal traffic volume within seconds.

I would therefore design the platform to handle between five and ten times the average traffic:

Average traffic: approximately 15,000 RPS
Expected peak: approximately 75,000 RPS
Extreme peak: approximately 150,000 RPS

The objective would not be to keep enough infrastructure running permanently for 150,000 requests per second. Instead, the platform should be able to scale towards that volume quickly and safely.

High-Level Architecture

The architecture would be divided into several layers:

Clients and integrations
        │
        ▼
Anycast DNS
        │
        ▼
CDN, WAF and DDoS protection
        │
        ▼
Global load balancer
        │
        ├── Europe region
        ├── North America region
        └── Asia-Pacific region
                │
                ▼
Regional load balancer or Kubernetes ingress
                │
                ▼
Go API services
        │
        ├── Redis Cluster
        ├── Transactional databases
        ├── Distributed databases
        └── Apache Kafka
                │
                ▼
Asynchronous Go consumers
        │
        ├── Notifications
        ├── Search indexing
        ├── Analytics
        ├── External integrations
        └── Object storage

The main request flow would be:

Client
→ CDN and WAF
→ Global load balancer
→ Nearest healthy region
→ API gateway or ingress
→ Go services
→ Redis, database or Kafka
→ Asynchronous processing

1. CDN, WAF and DDoS Protection

The first layer should prevent unnecessary or malicious requests from reaching the internal services.

I would use a platform such as Cloudflare, AWS CloudFront or Fastly to provide:

  • Content delivery network functionality
  • Edge caching
  • DDoS protection
  • Web Application Firewall protection
  • Bot detection and mitigation
  • Rate limiting
  • TLS termination
  • Geographical routing
  • Protection against common web attacks

Whenever possible, public or semi-public responses should be served directly from the edge.

For example, if 30% of all requests could be answered by the CDN, the internal services would avoid processing approximately 12 billion requests per month.

This reduction would directly affect:

  • Infrastructure costs
  • CPU consumption
  • Database utilisation
  • Application latency
  • Overall platform stability

The best request for the application infrastructure to process is the request that never reaches it.

2. Multi-Region Architecture

For a global and business-critical platform, I would deploy the system across at least three regions.

For example:

  • Europe
  • North America
  • Asia-Pacific

A global load balancer would direct each user to the nearest healthy region.

The strategy could include:

  • Active-active regions for APIs
  • Automatic regional failover
  • Asynchronous replication for eventually consistent data
  • A primary region for operations requiring strong consistency
  • Regional storage for regulated or residency-sensitive data

A multi-region architecture is not only about improving latency. It also protects the system against:

  • The failure of an entire cloud region
  • Large-scale networking problems
  • Cloud provider incidents
  • Faulty deployments
  • Operational disasters

3. Cell-Based Architecture

Rather than running every customer inside one enormous shared cluster, I would divide the platform into independent cells.

Each cell could contain:

  • Go services
  • A dedicated Redis cluster or namespace
  • A database or group of database partitions
  • Dedicated Kafka topics or partitions
  • Independent resource limits
  • Independent monitoring
  • A defined capacity target

For example:

Cell 01: customers 1–10,000
Cell 02: customers 10,001–20,000
Cell 03: customers 20,001–30,000

A routing service could use a tenant_id, customer_id or user_id to determine which cell should process a request.

The main benefit is reducing the blast radius of failures.

If one cell experiences a problem, only a percentage of customers should be affected. The remaining cells can continue operating normally.

It also becomes possible to add new cells as the platform grows, without scaling every part of the infrastructure at the same time.

4. API Services Written in Go

Go would be an excellent choice for the API layer because it provides:

  • Low memory consumption
  • Fast start-up times
  • Efficient concurrency through goroutines
  • Strong networking performance
  • Small, self-contained binaries
  • Simple container deployments
  • Excellent profiling tools
  • A mature cloud-native ecosystem

However, I would avoid creating unlimited goroutines for every operation.

Goroutines are lightweight, but they still consume memory, database connections, network sockets and downstream capacity.

The services should therefore implement:

  • Concurrency limits
  • Connection pooling
  • Request timeouts
  • Cancellation with context.Context
  • Backpressure
  • Circuit breakers
  • Limited retries
  • Graceful shutdown
  • Health-check endpoints
  • Metrics for every important route

A basic Go HTTP server could begin with configuration similar to this:

server := &http.Server{
    Addr:              ":8080",
    Handler:           router,
    ReadHeaderTimeout: 2 * time.Second,
    ReadTimeout:       5 * time.Second,
    WriteTimeout:      10 * time.Second,
    IdleTimeout:       60 * time.Second,
}

Every external dependency should also have an explicit timeout:

ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

A request should never wait indefinitely for another service to respond.

Internal Communication

For communication between services, I would consider:

  • HTTP and JSON for public APIs
  • gRPC or Connect for internal synchronous communication
  • Kafka for asynchronous events
  • Protocol Buffers for high-volume internal contracts

Not every interaction needs to go through Kafka.

Operations requiring an immediate response can use HTTP or gRPC. Kafka should be used when processing can happen asynchronously or when multiple services need to react to the same event.

5. Kubernetes and Autoscaling

The services could run on Kubernetes using a managed platform such as Amazon EKS, Google Kubernetes Engine or Azure Kubernetes Service.

Each service should have:

  • Multiple replicas
  • Pod Disruption Budgets
  • Readiness probes
  • Liveness probes
  • Resource requests
  • Resource limits
  • Topology spread constraints
  • Anti-affinity across availability zones
  • Automatic horizontal scaling

I would not scale services using CPU consumption alone.

Autoscaling decisions should consider:

  • CPU utilisation
  • Memory consumption
  • Requests per second
  • Active request concurrency
  • p95 and p99 latency
  • Internal queue sizes
  • Kafka consumer lag
  • Open connections

For example, an API service could scale when:

CPU utilisation exceeds 65%
or
p95 latency exceeds 200 ms
or
active requests per pod exceed 500

For Kafka consumers, consumer lag would be one of the most important scaling signals.

6. Redis for Caching and Temporary Data

Redis would reduce pressure on the databases and provide fast access to short-lived data.

Potential use cases include:

  • Response caching
  • User sessions
  • Rate limiting
  • Distributed locks
  • Idempotency keys
  • Feature flags
  • Counters
  • Temporary state
  • Frequently accessed query results

The target should be a high cache-hit ratio.

For example:

Cache-hit ratio: 90%
Requests reaching the database: 10%

If the platform receives 15,000 requests per second and Redis serves 90% of the required data, the database could receive approximately 1,500 queries per second instead of 15,000.

Redis should not automatically be treated as the permanent source of truth.

The services should also support a controlled degradation mode if the cache becomes unavailable.

7. Database Strategy

There is no single database that is ideal for every type of workload.

I would use different database technologies for different responsibilities.

PostgreSQL

PostgreSQL would be suitable for:

  • Relational data
  • Payments
  • Configuration
  • Accounts
  • Permissions
  • Transactional operations
  • Data requiring strong consistency

Depending on the volume, the PostgreSQL layer could use:

  • Table partitioning
  • Read replicas
  • PgBouncer
  • Tenant-based sharding
  • Independent databases for each cell
  • A managed solution such as Amazon Aurora PostgreSQL

Distributed Key-Value or Wide-Column Database

For very high-volume data accessed through predictable keys, I would consider:

  • Amazon DynamoDB
  • ScyllaDB
  • Apache Cassandra
  • Google Cloud Bigtable

These technologies can be suitable for:

  • Device state
  • Activity timelines
  • Counters
  • Large event histories
  • Write-heavy workloads
  • Predictable key-based queries

ClickHouse

For analytics and queries across large event datasets, I would use ClickHouse.

Possible workloads include:

  • Business reports
  • Usage metrics
  • Behavioural analysis
  • Large aggregations
  • Operational dashboards
  • Audit analysis

Running large analytical queries directly against the transactional database would be a mistake.

The database responsible for the product’s day-to-day operations should not also act as its data warehouse.

Object Storage

Historical data, exports and raw events could be stored in:

  • Amazon S3
  • Google Cloud Storage
  • Azure Blob Storage
  • MinIO

Object storage is considerably cheaper than transactional storage and can later support data reprocessing, analytics or machine-learning workloads.

8. Kafka as the Event Backbone

Kafka would become the backbone of the asynchronous processing layer.

When an important operation occurs, the responsible service would publish an event.

For example:

{
  "event_id": "01JXYZ123",
  "event_type": "order.created",
  "event_version": 1,
  "tenant_id": "tenant-123",
  "order_id": "order-987",
  "occurred_at": "2026-07-12T10:30:00Z"
}

Other services could consume the event to:

  • Send notifications
  • Update a search index
  • Record analytical data
  • Refresh reports
  • Execute fraud checks
  • Synchronise external systems
  • Populate a data lake
  • Invalidate caches

This prevents the API from having to complete every secondary operation before responding to the client.

The synchronous flow could be limited to:

  1. Validate the request
  2. Persist the main operation
  3. Publish or prepare the event
  4. Return the response

Everything else could happen asynchronously.

9. Preventing Dual Writes with the Transactional Outbox Pattern

A common problem occurs when a service needs to:

  1. Write data to a database
  2. Publish an event to Kafka

Imagine that the database write succeeds, but publishing the Kafka message fails. The system would be left in an inconsistent state.

I would use the Transactional Outbox pattern to solve this problem.

Within the same database transaction, the service would write:

  • The main business data
  • A record in an outbox table
BEGIN;

INSERT INTO orders (...);

INSERT INTO outbox_events (
    event_id,
    event_type,
    payload,
    created_at
) VALUES (...);

COMMIT;

A separate process would then publish the pending outbox records to Kafka.

Change Data Capture with a platform such as Debezium could also be used to capture and publish these events.

This pattern reduces the risk of losing events between the transactional database and Kafka.

10. Kafka Partitioning

The number of Kafka partitions should not be chosen arbitrarily.

It depends on:

  • Producer throughput
  • Consumer throughput
  • Required level of parallelism
  • Average message size
  • Number of consumer instances
  • Ordering requirements

A partition key could use:

tenant_id
customer_id
order_id
device_id

Events associated with the same identifier would be sent to the same partition, preserving ordering for that key.

However, the system must avoid hot partitions.

A customer with substantially more traffic than everyone else could concentrate too many messages in one partition.

In that situation, logical sharding could be introduced:

tenant-123:0
tenant-123:1
tenant-123:2
tenant-123:3

The final number of partitions should be selected using realistic benchmarks rather than theoretical estimates alone.

11. Idempotent Consumers

Kafka commonly operates with at-least-once delivery semantics. This means that the same event may occasionally be delivered more than once.

Consumers must therefore be idempotent.

Each event should have a unique identifier:

event_id: 01JXYZ123

Before executing an irreversible operation, the consumer should determine whether the event has already been processed.

This is particularly important for:

  • Payments
  • Order creation
  • Account balances
  • Issuing credits or benefits
  • Sending notifications
  • External integrations

Processing the same event twice should produce the same final result as processing it once.

12. Retries and Dead-Letter Queues

Temporary failures are normal in distributed systems.

Consumers should use retries with:

  • Exponential backoff
  • Jitter
  • A maximum number of attempts
  • Dedicated retry topics
  • A dead-letter queue

For example:

orders.events
orders.retry.1m
orders.retry.10m
orders.retry.1h
orders.dlq

A problematic message should not be retried indefinitely inside the main topic.

Otherwise, one invalid or unprocessable message could block or degrade an entire partition.

13. Backpressure and Load Shedding

When demand temporarily exceeds capacity, the platform needs to protect itself.

This can be achieved through:

  • Bounded queues
  • Concurrency limits
  • Rate limiting
  • Circuit breakers
  • Early rejection
  • Degraded responses
  • Prioritisation of critical operations

It is often better to reject a small percentage of requests quickly than to allow every service to fail slowly.

Operations could be classified by priority:

Priority 1: authentication, payments and core operations
Priority 2: standard product data
Priority 3: reports and exports
Priority 4: non-essential background operations

During periods of overload, lower-priority operations could be temporarily restricted.

14. Resilience Between Services

Every external dependency should have:

  • A timeout
  • A circuit breaker
  • Limited retries
  • Bulkhead isolation
  • A fallback strategy
  • Dedicated metrics

Retries should only be used for safe or idempotent operations.

An inappropriate retry strategy can multiply an incident.

For example:

Service A retries three times
Service B retries three times
Service C retries three times

A single original request could produce up to 27 internal attempts.

This is known as retry amplification and can turn a minor slowdown into a complete outage.

15. Observability

A platform operating at this scale must allow the engineering team to answer three questions quickly:

What is happening?
Where is the problem?
Which customer or operation is affected?

I would use OpenTelemetry to standardise:

  • Metrics
  • Distributed traces
  • Logs
  • Context propagation between services

The observability stack could include:

  • Prometheus
  • Grafana
  • Loki
  • Tempo
  • OpenSearch
  • Datadog
  • New Relic
  • Honeycomb

Important API Metrics

  • Requests per second
  • p50, p95 and p99 latency
  • Error rate
  • Timeout rate
  • Requests by endpoint
  • Active connections
  • Number of goroutines
  • Memory consumption
  • Garbage collector pauses

Important Kafka Metrics

  • Messages produced
  • Messages consumed
  • Consumer lag
  • Throughput by partition
  • Under-replicated partitions
  • Average message size
  • Processing duration
  • Dead-letter queue volume

Important Database Metrics

  • Active connections
  • Queries per second
  • Slow queries
  • Lock wait time
  • Replication lag
  • Cache-hit ratio
  • CPU and storage utilisation
  • Throttling events

Every log entry should contain identifiers such as:

trace_id
request_id
tenant_id
user_id
region
service
version

This would allow an engineer to follow a request from the edge all the way to the final Kafka consumer.

16. Service-Level Objectives and Error Budgets

I would define clear reliability targets.

For example:

Availability: 99.99%
p95 latency: below 200 ms
p99 latency: below 500 ms
Error rate: below 0.1%

An availability target of 99.99% allows approximately 52 minutes of downtime per year.

I would also use error budgets.

If the engineering team consumed the error budget too quickly, feature deployments could be reduced or paused until the platform returned to an acceptable level of reliability.

This makes reliability measurable rather than subjective.

17. Security

Security must exist at every layer of the architecture.

The platform should include:

  • Web Application Firewall protection
  • DDoS protection
  • Rate limiting by IP address, user and tenant
  • OAuth 2.0 or OpenID Connect
  • Short-lived access tokens
  • Key rotation
  • TLS encryption in transit
  • Encryption at rest
  • Centralised secrets management
  • Least-privilege access policies
  • Network segmentation
  • Audit logging
  • Container vulnerability scanning
  • A Software Bill of Materials
  • Signed build artefacts
  • CI/CD supply-chain protection

Internal services could use mutual TLS or workload identities.

Secrets should never be embedded inside Docker images or stored directly in the source-code repository.

18. Deployment Strategy

A high-volume platform should not deploy a new version directly to 100% of users.

I would use:

  • Canary deployments
  • Blue-green deployments
  • Feature flags
  • Automatic rollbacks
  • Progressive delivery

For example:

1% of traffic
5% of traffic
20% of traffic
50% of traffic
100% of traffic

During each stage, the platform should evaluate:

  • Error rate
  • Latency
  • CPU utilisation
  • Memory consumption
  • Kafka consumer lag
  • Business metrics

If a regression is detected, the deployment should be interrupted or rolled back automatically.

19. Load Testing

No architecture should be considered capable of processing 40 billion requests per month simply because the diagram looks correct.

It must be tested.

I would perform:

  • Load tests
  • Stress tests
  • Spike tests
  • Soak tests
  • Chaos engineering experiments
  • Regional failover tests
  • Kafka broker failure tests
  • Redis failure tests
  • Database degradation tests
  • Deployment rollback tests

Tools such as k6, Vegeta or Gatling could simulate realistic traffic patterns.

The tests should include:

  • Realistic payloads
  • Authentication
  • Different endpoint distributions
  • Cold caches
  • Warm caches
  • Large tenants
  • Hot keys
  • Regional traffic
  • Slow client connections
  • Partial dependency failures

The objective is not only to discover the maximum number of requests per second.

It is also necessary to understand how the system behaves when it reaches its limits.

A well-designed platform should fail in a predictable and controlled manner.

20. Network Traffic Estimate

The amount of network traffic depends heavily on the average response size.

If each response averages 5 KB:

40 billion × 5 KB
≈ 200 TB of response data per month

If each response averages 20 KB:

40 billion × 20 KB
≈ 800 TB of response data per month

These estimates do not include:

  • HTTP headers
  • Retries
  • Database replication
  • Internal service communication
  • Kafka messages
  • Logs
  • Distributed traces
  • Cross-region traffic

This is why compression, CDN caching, regional routing and careful payload design would have a significant effect on the final infrastructure cost.

The Complete Architecture

My reference architecture would include the following components:

Edge
├── Anycast DNS
├── CDN
├── WAF
├── DDoS protection
└── Rate limiting

Routing
├── Global load balancer
├── Regional load balancers
└── API gateway or Kubernetes ingress

Application
├── Services written in Go
├── HTTP and JSON for public APIs
├── gRPC or Connect for internal communication
├── Bounded concurrency
├── Timeouts
├── Circuit breakers
└── Graceful degradation

Asynchronous processing
├── Apache Kafka
├── Schema Registry
├── Transactional Outbox
├── Idempotent consumers
├── Retry topics
└── Dead-letter queues

Data
├── Redis Cluster
├── PostgreSQL
├── Distributed key-value database
├── ClickHouse
├── Search engine
└── Object storage

Infrastructure
├── Kubernetes
├── Multi-region architecture
├── Cell-based architecture
├── Autoscaling
├── Infrastructure as Code
└── Progressive delivery

Observability
├── OpenTelemetry
├── Prometheus
├── Grafana
├── Distributed tracing
├── Centralised logs
└── SLO-based alerting

Is Kafka Actually Necessary?

One important point is that processing 40 billion requests per month does not automatically mean that the platform needs Kafka.

Kafka would make sense if the product required:

  • Asynchronous processing
  • Multiple consumers for the same event
  • Event reprocessing
  • Integration between many services
  • Large event volumes
  • Analytical pipelines
  • Decoupling between business domains

For a predominantly synchronous and relatively simple API, introducing Kafka could add unnecessary operational complexity.

The architecture should be driven by the business requirements, not simply by the size of the monthly request estimate.

Conclusion

Forty billion requests per month equates to approximately 15,000 requests per second on average.

This volume can be handled by a modern distributed architecture, but not simply by adding more servers.

Scalability would come from the combination of:

  • Aggressive caching
  • Asynchronous processing
  • Data partitioning
  • Cell-based isolation
  • Specialised databases
  • Stateless services
  • Idempotent operations
  • Backpressure
  • Observability
  • Load testing
  • Operational automation

Go would provide a strong foundation for efficient and concurrent services. Kafka would decouple processes and help absorb temporary traffic peaks. Redis would reduce pressure on the databases. A multi-region, cell-based architecture would limit failures and support progressive growth.

However, the most important lesson is that 40 billion requests should not be treated simply as one large monthly number.

The system must be designed around:

Traffic peaks
Latency requirements
Consistency requirements
Message and payload sizes
Geographical distribution
External dependencies
Infrastructure costs
Failure behaviour

The most scalable architecture is not necessarily the one that uses the largest number of technologies.

It is the architecture in which every component has a clear responsibility, operational limits are understood and failures have been anticipated before they occur.

How process.nextTick() Can Starve the Node.js Event Loop

A Node.js service can look healthy while quietly becoming unresponsive.

The process is still running. The queue is still being drained. CPU usage may not even appear unusually high.

Yet something strange begins to happen:

  • HTTP health checks time out;
  • new WebSocket connections stop being accepted;
  • setTimeout() callbacks never fire;
  • the service appears alive, but it no longer responds properly.

The cause may be hidden in a seemingly harmless line:

process.nextTick();

Used carefully, process.nextTick() is useful. Used recursively, however, it can prevent the Node.js event loop from moving forward.

Consider this queue-processing code:

function drainQueue(queue) {
  if (queue.length === 0) return;
  const item = queue.shift();
  processNotification(item);
  process.nextTick(() => drainQueue(queue));
}

At first glance, this appears sensible.

The function removes one notification, processes it, and schedules the next iteration asynchronously.

Because the recursive call is not made directly, it may appear that the application is yielding control back to Node.js.

It is not.

The important detail about process.nextTick()

process.nextTick() does not behave like setTimeout() or setImmediate().

When the current JavaScript operation finishes, Node.js processes callbacks placed in the nextTick queue before allowing the event loop to continue through its normal phases.

Those phases include:

  • timers, where setTimeout() and setInterval() callbacks run;
  • poll, where Node.js handles network and filesystem I/O;
  • check, where setImmediate() callbacks run;
  • close callbacks, where certain resources are closed.

The nextTick queue has particularly high priority.

Node.js drains this queue before continuing with the event loop. Recursive process.nextTick() calls can therefore keep the application inside that queue indefinitely.

In our example, the flow looks like this:

drainQueue()

processNotification()

process.nextTick(drainQueue)

drainQueue()

process.nextTick(drainQueue)

...

Every callback places another callback into the same high-priority queue.

As long as notifications remain, the queue keeps refilling itself.

The event loop does not get a proper opportunity to move on.

What is event loop starvation?

Event loop starvation happens when one source of work continuously occupies the event loop and prevents other ready work from being processed.

The application is not necessarily dead or crashed.

It is still performing work, but it is unfairly prioritising one category of work so heavily that everything else is left waiting.

Imagine a meeting where one person repeatedly says:

“Just one more thing.”

Every time they finish speaking, they immediately introduce another point before anyone else gets a turn.

The meeting is technically progressing, but nobody else is able to contribute.

That is starvation.

In this Node.js example, the queue consumer continues processing notifications, but HTTP requests, WebSocket connections, timers and other callbacks are never given enough time to run.

Is this a Node.js-only problem?

The specific API shown here, process.nextTick(), is Node.js-specific.

It does not exist in normal browser JavaScript.

However, the broader problem of event loop starvation is not limited to Node.js.

Browsers have their own event loop, task queues and microtask queue. Promise callbacks and callbacks scheduled with queueMicrotask() run as microtasks.

After the current JavaScript task completes, the browser drains the microtask queue before moving to the next task. Microtasks are allowed to schedule more microtasks, and those newly created microtasks are also processed before the browser proceeds.

That means a browser can experience a similar starvation problem:

function keepRunning() {
  queueMicrotask(keepRunning);
}
keepRunning();
setTimeout(() => {
  console.log('This may never run');
}, 0);

Each microtask schedules another microtask.

The browser keeps draining the microtask queue and may never reach the timer callback.

The page may also stop responding to user interaction or fail to repaint because rendering generally needs an opportunity between event loop tasks.

The Node.js version

In Node.js, recursive process.nextTick() can starve:

  • timers;
  • filesystem and network I/O;
  • HTTP request handling;
  • WebSocket connections;
  • setImmediate() callbacks.

The browser version

In a browser, an endless microtask chain can starve:

  • setTimeout() and setInterval();
  • click, keyboard and other UI events;
  • network-related task callbacks;
  • animation frames;
  • screen rendering and visual updates.

So the broader lesson applies to both environments:

High-priority asynchronous work can still block an application if it continuously schedules more high-priority work.

The API is different, but the starvation pattern is very similar.

Does Promise recursion cause the same problem?

Potentially, yes.

Promise callbacks are placed in the microtask queue:

function loop() {
  Promise.resolve().then(loop);
}
loop();
setTimeout(() => {
  console.log('Timer');
}, 0);

This creates a continuously replenished microtask queue.

In a browser, it can prevent the next task and rendering opportunity.

In Node.js, Promise microtasks can also delay other event loop work, although process.nextTick() has its own Node-specific scheduling behaviour and is processed with especially high priority.

For this reason, neither process.nextTick() nor Promise microtasks should be used as an unbounded queue-processing mechanism.

A useful distinction

JavaScript itself does not define networking, timers, the DOM or process.nextTick().

Those features are provided by the runtime environment.

The same JavaScript language can therefore run under different event loop implementations:

EnvironmentHigh-priority mechanismWhat may be starved
Node.jsprocess.nextTick() and microtasksI/O, timers, HTTP, WebSockets and setImmediate()
BrowserPromise microtasks and queueMicrotask()timers, user events, rendering and animation frames

So it is more accurate to say:

Event loop starvation is a runtime scheduling problem, not exclusively a Node.js or browser problem.

The example in this article is Node.js-specific because it uses process.nextTick(), but the underlying concept also exists in browsers.

How to Recognise When a Hash Map Can Improve Your LeetCode Solution

When solving algorithm problems, one of the most useful skills is recognising patterns.

A common example is identifying when a hash map or hash set can replace repeated searches and reduce the time complexity of a solution.

In many cases, a problem that initially appears to require an O(n²) solution can be reduced to O(n) by using a hash-based data structure.

But how can you recognise these situations quickly?

What is a hash map?

A hash map stores data as key-value pairs.

const user = new Map();
user.set("name", "Mhayk");
user.set("age", 39);
console.log(user.get("name")); // Mhayk

Internally, the key is processed by a hash function, which determines where the value should be stored.

Hash maps usually provide the following average time complexities:

OperationAverage complexity
InsertO(1)
SearchO(1)
DeleteO(1)

The worst case can be O(n), particularly when many collisions occur, but modern implementations are designed to minimise this.

The main question to ask

When reading a problem, ask yourself:

Do I need to find out quickly whether I have already seen something?

If the answer is yes, a hash map or hash set may be useful.

This is especially true when your first solution contains nested loops.

for (let i = 0; i < numbers.length; i++) {  
  for (let j = i + 1; j < numbers.length; j++) {    
      // Compare every pair  
   }
}

This approach is usually O(n²) because each element is compared with many other elements.

A hash map can often replace the inner loop with a constant-time lookup.

Example: Two Sum

Consider the classic Two Sum problem:

Given an array of integers and a target, return the indices of two numbers whose sum is equal to the target.

A brute-force approach compares every possible pair.

function twoSum(numbers, target) {
  for (let i = 0; i < numbers.length; i++) {
    for (let j = i + 1; j < numbers.length; j++) {
      if (numbers[i] + numbers[j] === target) {
        return [i, j];
      }
    }
  }
  return [];
}

The time complexity is:

O(n²)

For each number, however, we already know which other value we need.

required value = target - current value

We can store previously seen values in a hash map.

function twoSum(numbers, target) {
  const seen = new Map();
  for (let i = 0; i < numbers.length; i++) {
    const requiredValue = target - numbers[i];
    if (seen.has(requiredValue)) {
      return [seen.get(requiredValue), i];
    }
    seen.set(numbers[i], i);
  }
  return [];
}

Now the array is traversed only once.

The average time complexity becomes:

O(n)

The space complexity is:

O(n)

This is a classic example of exchanging additional memory for better execution time.

When should you use a Set?

Use a Set when you only need to know whether a value exists.

For example:

Does the array contain any duplicate values?

function containsDuplicate(numbers) {
  const seen = new Set();
  for (const number of numbers) {
    if (seen.has(number)) {
      return true;
    }
    seen.add(number);
  }
  return false;
}

The set does not need to associate the value with any additional information.

It only answers:

Have I seen this value before?

When should you use a Map?

Use a Map when you need to associate a key with extra information.

Common examples include:

value -> index
character -> frequency
prefix sum -> number of occurrences
word signature -> group of words

For example, counting character frequencies:

function countCharacters(text) {
  const frequencies = new Map();
  for (const character of text) {
    const currentCount = frequencies.get(character) ?? 0;
    frequencies.set(character, currentCount + 1);
  }
  return frequencies;
}

Common LeetCode patterns involving hash maps

There are several recurring patterns where hash maps are particularly useful.

1. Detecting duplicates

Typical questions include:

  • Does the array contain duplicates?
  • Has this element appeared before?
  • Is there a repeated character?

A Set is usually the first structure to consider.

2. Counting frequencies

Typical questions include:

  • How many times does each value appear?
  • Which value appears most frequently?
  • Are two strings anagrams?

A hash map can store the frequency of each element.

frequency.set(value, (frequency.get(value) ?? 0) + 1);

3. Finding a complement

This pattern appears when one value must be combined with another to satisfy a condition.

Examples include:

  • two values whose sum equals a target;
  • two values whose difference equals a target;
  • checking whether a required counterpart exists.

For Two Sum, the complement is:

const complement = target - currentValue;

Instead of searching the entire array, you check the hash map.

4. Grouping elements

Hash maps are also useful when elements can be represented by a shared key.

For example, in Group Anagrams:

"eat"
"tea"
"ate"

All three words can be transformed into the same sorted key:

"aet"

The hash map can then associate that key with a list of words.

function groupAnagrams(words) {
  const groups = new Map();
  for (const word of words) {
    const key = word.split("").sort().join("");
    if (!groups.has(key)) {
      groups.set(key, []);
    }
    groups.get(key).push(word);
  }
  return [...groups.values()];
}

5. Prefix sums

Hash maps are frequently combined with prefix sums.

This happens in problems involving:

  • subarray sums;
  • counting subarrays;
  • finding previous cumulative values;
  • matching a current sum with an earlier sum.

Suppose the current prefix sum is:

currentSum

To find a subarray whose sum is k, we look for:

currentSum - k

If that value has appeared before, then a valid subarray exists.

This is one of the most important intermediate LeetCode patterns.

6. Sliding window problems

Hash maps and sets are also common in sliding window problems.

A typical example is:

Find the length of the longest substring without repeating characters.

The window moves through the string while a set tracks the characters currently inside it.

function lengthOfLongestSubstring(text) {
  const characters = new Set();
  let left = 0;
  let longest = 0;
  for (let right = 0; right < text.length; right++) {
    while (characters.has(text[right])) {
      characters.delete(text[left]);
      left++;
    }
    characters.add(text[right]);
    longest = Math.max(longest, right - left + 1);
  }
  return longest;
}

In this case, the set provides fast membership checks while the window expands and contracts.

A quick checklist

When reading a problem, ask these questions:

  1. Do I need to check whether something has appeared before?
  2. Do I need to count occurrences?
  3. Do I need fast membership checks?
  4. Am I repeatedly searching through an array?
  5. Do I have nested loops because I am comparing every pair?
  6. Can I transform each element into a useful key?
  7. Do I need to associate an element with an index, count or group?

If the answer to any of these is yes, consider using a Map or Set.

Hash map or sorting?

A hash map is not always the best option.

Sorting may be preferable when:

  • the order of the elements matters;
  • you need to process values from smallest to largest;
  • the problem can be solved with two pointers;
  • you want to reduce additional memory usage;
  • you need range-based comparisons.

For example, Two Sum can also be solved by sorting the values and using two pointers.

That solution usually takes:

O(n log n)

It may use less additional memory, but preserving the original indices can make the implementation more complicated.

Time versus space

Hash maps often improve execution time by using more memory.

A common transformation is:

Before:
Time: O(n²)
Space: O(1)

After:
Time: O(n)
Space: O(n)

This is known as a time-space trade-off.

You are using additional memory to avoid repeated work.

A useful mental shortcut

A helpful rule is:

If your solution repeatedly searches for something that has already been processed, store it in a hash map.

Another useful rule is:

If an inner loop exists only to check whether a value is present, try replacing it with Map.has() or Set.has().

These two questions alone can help identify many common LeetCode solutions.

Final thoughts

Hash maps are not simply data structures to memorise.

They are tools for avoiding repeated work.

The key skill is recognising when information from previous iterations can be stored and reused.

Whenever you see duplicates, frequencies, complements, grouping, previous values, prefix sums or fast existence checks, a hash map should be one of the first options you consider.

With enough practice, recognising these patterns becomes almost automatic.

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. 🚀

Optional Chaining in JavaScript

?.Optional Chaining operator

Image from twitter by Daniel Rosenwasser (@Daniel Rosenwasser).

Optional chaining will eliminate the need for manually checking if a property is available in an object. With option chaining the checking will be done internally.

Example without Optional chaining.

const planet1 =  {
    name: 'Earth',
    size: '6,371 km'
  }

const planet2 = {
    name: 'Mars',
    // size: '3,389.5 km'
  }

function printPlanetInfo(planet) {
  console.log(`Name: ${planet.name}`)
  console.log(`Size: ${planet.size.toUpperCase()}`)
}

printPlanetInfo(planet1)
printPlanetInfo(planet2)

When we pass an planet object which doesn’t have the size property:

To solve the above problem what we do is, we will add check if size property available in the planet object

const planet1 =  {
    name: 'Earth',
    size: '6,371 km'
  }

const planet2 = {
    name: 'Mars',
    // size: '3,389.5 km'
  }

function printPlanetInfo(planet) {
  console.log(`Name: ${planet.name}`)
  planet.size = ( planet && planet.size &&
                  planet.size.toUpperCase())
                || "undefined"
  console.log(`Size: ${planet.size}`)
}

printPlanetInfo(planet1)
printPlanetInfo(planet2)

Using Optional Chaining

The optional chaining will check if an object left to the operator is valid (not null and undefined). If the property is valid then it executes the right side part of the operator otherwise return undefined.

function printPlanetInfo(planet) {
  console.log(`Name: ${planet.name}`)
  planet.size = ( planet?.size?.toUpperCase() ) || "undefined"
  console.log(`Size: ${planet.size}`)
}

The native JavaScript equivalent code for above optional chaining operator is

(property == undefined || property == null) ? undefined : property

Using variables as property name

We can use variables as property name in optional chaining

const planet = {
  name: 'Earth',
  size: '6,371 km'
}

planet?.size

// We can also use with expressions

planet?.["s"+"ize"] 

Functional call with optional chaining

You can use optional chaining to call a method which may not exist.

const planet = {
  name: 'Earth',
  size: '6,371 km'
  getSize() {
    return this.size;
  }
}

planet?.getSize?.()

Reference: MDN.

Tree Data Structure

Usually when we start to study data structures, we study first trees and then graph. Can I give you two tips ? First read about graph theory. Try to answer for yourself the two following questions:

  • What are acyclic and cyclic graphs ?
  • What are directed, undirected and bidirectional graphs ?

Right. Are you a bit more confused now ? 🙂 Let’s blow the candles and see the knowledge emerging like magic. Let’s list some conditions to say: “Hey it is a tree !”

  • Just like in real life, there are many types of trees data structures that we can have.
  • In graph theory, a tree is a nonlinear data structure, compared to arrays, linked lists, stacks and queues which are linear data structures.
  • One of the disadvantages of using an array or linked list to store data is the time necessary to search for an item. Since both the arrays and Linked Lists are linear structures the time required to search a “linear” list is proportional to the size of the data set.
  • Every tree is a graph, but not every graph is a tree.
    • A is the root but it also a parent. In graph theory, a loop (also called a self-loop or a “bucle”) is an edge that connects a vertex to itself. A tree don’t have loop.
  • A child has just one parent.
  • A tree is acyclic(contains no cycles).