Japan's recycling system is one of the most thorough in the world, and one of the most difficult to navigate if you are new to it.
There are 1,741 municipalities in Japan, and almost every one has different rules. A PET bottle is not a single waste item: the body, cap, and label are three separate categories, and which bins they go into depends on where you live. The official guidance lives in printed pamphlets, often only in Japanese, updated annually.
I built Gomi Bunrui to try and help with this. You point your phone camera at any waste item, get a breakdown of every component, and see exactly where each part goes based on the rules for your specific city.
This post covers the technical details. The full competition write-up is on AWS Builder Center if you want the broader story.
The Stack
- Frontend: React PWA deployed to S3 and CloudFront
- Auth: Amazon Cognito with email and social login, email confirmation, resend confirmation, and password reset
- Backend: Kotlin Lambda with SnapStart, http4k framework, behind API Gateway
- AI: Amazon Bedrock (Claude Haiku 4.5)
- Data: DynamoDB
- DNS/TLS: Cloudflare for domain registration, Route 53 for DNS, ACM for TLS certificates
- IaC: AWS SAM covering everything, zero clickops
- CI/CD: GitHub Actions across a four-project monorepo (backend, frontend, website, e2e tests) with semantic versioning and full test coverage
- Dev tooling: Kiro IDE with custom hooks and a conventional commits agent skill
The Core Challenge: One Item, Multiple Waste Categories
The interesting problem was getting Amazon Bedrock to reliably decompose a single physical object into its constituent materials, rather than returning a single category for the whole thing.
A PET bottle is the clearest example:
| Component | Classification |
|---|---|
| Bottle body | PET bottle recycling |
| Plastic cap | Plastics |
| Label | Burnable waste |
One object. Three bins. Three different collection days.
What did not work
Prompting the model to "classify this item" returns a single answer. Reasonable, but wrong for this use case.
What worked
Reframing the instruction to focus on materials rather than the object as a whole:
Identify every physical component present in the image
and classify each one independently against the city's
waste category rules.
Combined with explicit multi-component examples in the prompt and a response parser that flags responses where the component count seems too low for the identified item type.
The response schema
{
"item_name": {"en": "Sports Drink PET Bottle", "ja": "スポーツドリンクPETボトル"},
"component_count": 3,
"category_count": 3,
"components": [
{
"name": {"en": "Bottle Body", "ja": "ボトル本体"},
"material": {"en": "PET resin", "ja": "PET樹脂"},
"category_id": "pet_bottle",
"preparation": {"en": "Rinse with water", "ja": "水で軽くすすぐ"},
"icon": "♻️"
}
],
"tips": [{"en": "...", "ja": "..."}],
"difficulty": "easy|moderate|hard"
}
A few deliberate decisions here:
Human-readable fields use {"en": "...", "ja": "..."} bilingual objects so the frontend can display in either language without a second API call.
category_id is an English constant string and is never translated by the model. The frontend maps category IDs to colours and display names using a static JSON file, which keeps the UI consistent and reduces token usage.
difficulty is a plain enum. Translations live in static i18n files rather than being generated by Bedrock on each request.
The result is that Japanese-only human-readable text combined with English constants roughly halves inference cost compared to fully bilingual model output.
Scaling Across Municipalities
All four supported cities (Nagoya, Shibuya, Osaka, Kyoto) share an identical JSON schema. Only the category definitions differ per city:
Nagoya prompt = shared classification rules + Nagoya category definitions
Shibuya prompt = shared classification rules + Shibuya category definitions
Osaka prompt = shared classification rules + Osaka category definitions
Kyoto prompt = shared classification rules + Kyoto category definitions
Adding a new municipality requires no code changes at all. The work is extracting the rules from official PDFs, verifying them against source material, and writing a new prompt. Scaling is a data task rather than an engineering task.
Guardrails
Four rejection types are defined, all returning 200 OK with "error": true in the response body:
| Error Code | Situation |
|---|---|
inappropriate_content |
Obviously inappropriate input |
prompt_injection |
Injection attempt detected |
unrecognizable_image |
Cannot identify as a waste item |
hazardous_item |
Medical waste or similar |
HTTP 4xx and 5xx codes are reserved for infrastructure failures only. The frontend reads the error code from the response body and shows the appropriate UI state.
The tricky part was calibration. Early versions were too aggressive and flagging normal product labels and foreign-language packaging as potential prompt injection. It took several prompt iterations to get the balance right between rejecting genuinely problematic inputs and allowing ordinary real-world items.
Infrastructure
Everything is defined as code using AWS SAM. SAM templates cover Lambda, API Gateway, DynamoDB, S3, CloudFront, Cognito, SES, CloudWatch (structured logs, custom metrics under the GomiBunrui namespace, and alarms), Route53, and ACM. There is no clickops anywhere in the stack.
The project is a monorepo with four independently deployable projects: backend, frontend, website, and end-to-end tests. Each follows semantic versioning driven by conventional commits. The CI/CD pipeline runs the full test suite on every push and only deploys on a clean run.
One note on DNS: Route53 does not support .app domain registration, so Cloudflare handles domain registration while Route53 manages the hosted zone. Both are defined in SAM.
The Kiro Workflow
The entire project was built spec-first using Kiro IDE. Rather than jumping straight into code, each feature started with a spec that Kiro translated into requirements, a design document, and a task list.
Three custom hooks made the workflow fit more naturally:
Git-commit hook: fires after every completed top-level task and commits automatically. The git log ends up being a readable record of the build progression.
Sequential ordering hook: fires before task implementation begins and adds a sequence number to the spec directory name. Makes it easy to see the order in which things were built.
Knowledge-base hook: fires at the end of each implementation phase and updates the project's living documentation automatically, covering ubiquitous language, architecture decisions, infrastructure details, and e2e test coverage.
A custom agent skill for conventional commits feeds into semantic versioning across all four monorepo projects.
What I Took Away From This
The spec-first workflow felt like it slowed things down at first, but it consistently surfaced decisions early that would have been painful to revisit mid-implementation. The navigation architecture, bilingual output schema, and DynamoDB structure were all settled before a line of code was written.
The AWS Free Tier constraints turned out to be useful rather than limiting. Lambda with SnapStart over a persistent server, DynamoDB on-demand over provisioned, aggressive CloudFront caching: these are also the right choices for a production system at this scale, so the constraints pointed in the right direction.
The most unexpected learning was that prompt engineering and product design are the same activity at this layer of the stack. The quality of the scan results is almost entirely a function of the quality of the system prompts. Getting that right took more careful iteration than any part of the infrastructure.
Try It
The app is live as a PWA at gomi-bunrui.app. Registration and scanning are fully functional across all four cities. No app store required.
If you are building with Bedrock or Kiro, happy to answer questions in the comments.
If you would like to support the project in the AWS AIdeas competition, a like on the article helps it reach the top 300. Voting closes 20 March.
Built with Kiro and the AWS Free Tier as part of the AWS 10,000 AIdeas competition.


Top comments (0)