← Back to Engineering

Benchmarking Ether vs. Cursor: Achieving Better Results with 40% Less Token Usage

2026-08-28Overclock

As AI coding assistants become deeply integrated into our daily workflows, engineering teams are running into a frustrating wall: context bloat. Modern enterprise applications span multiple repositories, microservices, and databases. When you ask an AI a complex architectural question, brute-force ingestion often leads to massive token bills and hallucinated answers.

Better context reduces token consumption and delivers more precise results.

To prove this, I ran a head-to-head benchmark using a well-known, highly complex open-source project: Spring PetClinic Microservices. This repository separates logic across an API Gateway, Config Server, Discovery Server, and multiple backend services. It is the perfect stress test for an AI's ability to navigate cross-module logic without getting lost in the noise.

Tools:

  1. Ether (Model: Kimi k3)
  2. Cursor (Free Plan, Model: Auto)

The Benchmark Prompt

"Trace the exact cross-service data flow when a user requests an owner's details via the API Gateway (GET /api/gateway/owners/{ownerId}). Identify the specific class and method in the api-gateway module that aggregates this data. Then, list the exact REST clients (WebClient or Feign) it uses to call downstream services, the specific controller classes and methods triggered in the customers-service and visits-service, and the underlying database tables queried in both backend microservices to assemble the final response. Do not generate any code; just provide a step-by-step architectural trace with the exact file names."


Tool 1: Ether (Kimi k3)

The Analysis

Ether utilized its context-pruning orchestration to filter out irrelevant files before querying the LLM. It traced the entire stack, identifying layers that Cursor completely missed.

Token Consumption

Ether Token Consumption

Ether Output:

Cross-Service Data Flow Trace: GET /api/gateway/owners/{ownerId}

1. Entry Point (Browser / Frontend)

The AngularJS UI issues the request from:

  • spring-petclinic-api-gateway/src/main/resources/static/scripts/owner-details/owner-details.controller.js$http.get('api/gateway/owners/' + $stateParams.ownerId)

This hits the API Gateway, which is the single ingress point for the Angular app (routes configured in spring-petclinic-api-gateway/src/main/resources/application.yml).


2. API Gateway — Aggregation Layer

File: spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiGatewayController.java

  • Class: ApiGatewayController (annotated @RestController + @RequestMapping("/api/gateway"))
  • Method: getOwnerDetails(int ownerId) mapped via @GetMapping(value = "owners/{ownerId}")
  • Return type: Mono<OwnerDetails> (fully reactive, non-blocking WebFlux)

The aggregation logic is:

  1. Call customersServiceClient.getOwner(ownerId) → returns Mono<OwnerDetails> containing the owner and their pets (but no visits yet).
  2. flatMap on the owner → extract pet IDs via owner.getPetIds().
  3. Call visitsServiceClient.getVisitsForPets(petIds) → returns Mono<Visits>.
  4. Wrap the visits call in a Resilience4j circuit breaker (cbFactory.create("getOwnerDetails")), with fallback emptyVisitsForPets() (returns empty visits list on failure — graceful degradation).
  5. .map(addVisitsToOwner(owner)) merges visits into each pet (matching on petId) and returns the enriched OwnerDetails.

3. Downstream REST Clients (both use WebClient, not Feign)

A. Customers client

File: spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/CustomersServiceClient.java

  • Class: CustomersServiceClient
  • Method: getOwner(int ownerId)
  • HTTP call: GET http://customers-service/owners/{ownerId} (uses the @LoadBalanced WebClient.Builder defined in ApiGatewayApplication.java, so service discovery resolves the customers-service hostname)

B. Visits client

File: spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/VisitsServiceClient.java

  • Class: VisitsServiceClient
  • Method: getVisitsForPets(List<Integer> petIds)
  • HTTP call: GET http://visits-service/pets/visits?petId={comma-separated IDs} (joins IDs via joinIds())

4. Downstream Controllers Triggered

A. Customers Service

File: spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java

  • Class: OwnerResource (@RestController, base path /owners)
  • Method: findOwner(int ownerId)@GetMapping("/{ownerId}")
  • Delegates to OwnerRepository.findById(ownerId) (Spring Data JPA).
  • The returned Owner entity eagerly/fetch-joins its pets collection (and each pet's PetType), which is what populates the pet IDs/names/birthDates/types in the gateway's OwnerDetails.

B. Visits Service

File: spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java

  • Class: VisitResource (@RestController)
  • Method: read(@RequestParam("petId") List<Integer> petIds)@GetMapping("pets/visits")
  • Delegates to VisitRepository.findByPetIdIn(petIds) (spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java)
  • Wraps the result in its inner record Visits(List<Visit> items) and returns it.

5. Underlying Database Tables Queried

Customers Service DB

Schema files:

  • spring-petclinic-customers-service/src/main/resources/db/hsqldb/schema.sql
  • spring-petclinic-customers-service/src/main/resources/db/mysql/schema.sql

Tables touched when OwnerRepository.findById() fetches the owner graph:

TableRole
ownersOwner row (id, first_name, last_name, address, city, telephone)
petsPets belonging to the owner (FK owner_id → owners.id)
typesPet type lookup for each pet (FK pets.type_id → types.id)

Visits Service DB

Schema files:

  • spring-petclinic-visits-service/src/main/resources/db/hsqldb/schema.sql
  • spring-petclinic-visits-service/src/main/resources/db/mysql/schema.sql
TableRole
visitsRows filtered via WHERE pet_id IN (...) by findByPetIdIn() — columns id, pet_id, visit_date, description

End-to-End Sequence Summary

Browser (Angular owner-details.controller.js)
   │  GET /api/gateway/owners/{ownerId}
   ▼
ApiGatewayController.getOwnerDetails()                      [api-gateway]
   ├─► CustomersServiceClient.getOwner(ownerId)   (WebClient)
   │      GET http://customers-service/owners/{ownerId}
   │      ▼
   │      OwnerResource.findOwner(ownerId)                 [customers-service]
   │      ▼  OwnerRepository.findById()
   │      Tables: owners, pets, types  ◄── returns OwnerDetails(+pets)
   │
   ├─► extract owner.getPetIds()
   │
   └─► VisitsServiceClient.getVisitsForPets(petIds) (WebClient + Resilience4j CB "getOwnerDetails")
          GET http://visits-service/pets/visits?petId=1,2,...
          ▼
          VisitResource.read(petIds)                        [visits-service]
          ▼  VisitRepository.findByPetIdIn()
          Table: visits  ◄── returns Visits(items)
   ▼
addVisitsToOwner() merges visits into each pet (filter by petId)
   ▼
Mono<OwnerDetails> serialized → JSON response to browser

Key architectural notes:

  • The gateway performs client-side service composition (API composition pattern) — the two downstream calls are sequential in code (flatMap), because the visits call depends on pet IDs from the owner response.
  • Resilience is handled by a Resilience4j ReactiveCircuitBreaker named "getOwnerDetails" with a 10-second TimeLimiterConfig (default in ApiGatewayApplication.java); on visits-service failure the owner data is still returned with empty visit lists.
  • Both clients use @LoadBalanced WebClient.Builder (defined in spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java) with Eureka service discovery — there is no Feign in this path.

Tool 2: Cursor (Auto)

The Analysis

Cursor provided a solid, accurate trace of the backend Java services. It explored 17 files and made 13 searches. It correctly identified that the project uses WebClient rather than Feign, and it mapped the downstream JPA repositories.

Token Consumption

Ether Token Consumption

Cursor Output:

Here is the exact cross-service trace for GET /api/gateway/owners/{ownerId}.

Overview

This is a BFF-style aggregation in spring-petclinic-api-gateway. The gateway does not use Feign anywhere in this project. Both downstream calls use WebClient (via a @LoadBalanced WebClient.Builder bean in ApiGatewayApplication.java).

Step 1 — API Gateway entry point

File: spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiGatewayController.java

This method orchestrates the full response:

  1. Calls customersServiceClient.getOwner(ownerId) to fetch owner + pets.
  2. On success, calls visitsServiceClient.getVisitsForPets(owner.getPetIds()).
  3. Wraps the visits call in a Resilience4j reactive circuit breaker named "getOwnerDetails" (falls back to emptyVisitsForPets() on failure).
  4. Merges visits into each pet via the private helper addVisitsToOwner(OwnerDetails owner).
  5. Returns Mono<OwnerDetails>.

Pet IDs are extracted in OwnerDetails.getPetIds() (spring-petclinic-api-gateway/.../dto/OwnerDetails.java) from the pets list returned by customers-service.

Step 2 — Downstream REST clients (WebClient only)

2a. Customers client File: spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/CustomersServiceClient.java

  • Class: CustomersServiceClient
  • Method: getOwner(int ownerId)
  • Target: GET http://customers-service/owners/{ownerId}

2b. Visits client File: spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/VisitsServiceClient.java

  • Class: VisitsServiceClient
  • Method: getVisitsForPets(List<Integer> petIds)
  • Target: GET http://visits-service/pets/visits?petId={id1,id2,...}

Service names (customers-service, visits-service) are resolved through Spring Cloud LoadBalancer because the WebClient.Builder is annotated @LoadBalanced in ApiGatewayApplication.java.

Step 3 — customers-service

File: spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java

  • Method: findOwner(int ownerId)
  • Effective endpoint: GET /owners/{ownerId}
  • Persistence layer: OwnerRepository.findById(ownerId)

Because Owner has @OneToMany(fetch = FetchType.EAGER) on pets, and each Pet has @ManyToOne on PetType, a single owner lookup triggers reads from three tables:

  • owners: Primary lookup via findById
  • pets: Eager @OneToMany(mappedBy = "owner")
  • types: @ManyToOne @JoinColumn(name = "type_id") on each Pet

The JSON response includes owner fields and pets (with type), but no visits — those are added later by the gateway.

Step 4 — visits-service

File: spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java

  • Method: read(List<Integer> petIds)
  • Effective endpoint: GET /pets/visits?petId=1,2,3
  • Persistence layer: VisitRepository.findByPetIdIn(Collection<Integer> petIds)

Database table queried:

  • visits (SELECT ... FROM visits WHERE pet_id IN (...))
  • Schema definition: spring-petclinic-visits-service/src/main/resources/db/mysql/schema.sql

Step 5 — Gateway aggregation and final response

Back in ApiGatewayController.addVisitsToOwner():

  • Iterates each PetDetails in the owner.
  • Filters Visits.items() where visit.petId() == pet.id().
  • Appends matching visits into that pet's visits list.
  • Returns the enriched OwnerDetails.

Cursor End-to-End Sequence Summary

ApiGatewayController.getOwnerDetails()                      [api-gateway]
   ├─► CustomersServiceClient.getOwner(ownerId)   (WebClient)
   │      GET http://customers-service/owners/{ownerId}
   │      ▼
   │      OwnerResource.findOwner(ownerId)                 [customers-service]
   │      ▼  OwnerRepository.findById()
   │      Tables: owners, pets, types  ◄── returns OwnerDetails(pets, no visits)
   │
   ├─► extract owner.getPetIds()
   │
   └─► VisitsServiceClient.getVisitsForPets(petIds) (WebClient)
          GET http://visits-service/pets/visits?petId=1,2,...
          ▼
          VisitResource.read(petIds)                        [visits-service]
          ▼  VisitRepository.findByPetIdIn()
          Table: visits  ◄── returns Visits JSON
   ▼
addVisitsToOwner() merge
   ▼
OwnerDetails (owner + pets + visits)

The Final Verdict

Metric / AspectCursor (Auto)Ether (Kimi k3)Winner
Gross Context Usage~31.8K Tokens26.1K TokensEther (18% cost efficiency)
Active / Billed Tokens~31.8K Tokens18.4K Tokens (7.7K cached)Ether (42% cost efficiency)
Tracing DepthStarted at API GatewayTraced back to AngularJS UIEther
Service DiscoveryOmittedIdentified Eureka ResolutionEther
Database SchemaInferred via JPALocated physical .sql filesEther