How I Built a Fully Automated Asian Tech News Newsletter with Spring Boot + Gemini AI
From RSS ingestion to your inbox — zero manual work.
The Problem
Asian tech markets move fast. Chinese AI startups, semiconductor breakthroughs, EV innovations — most of it never makes it to Western tech media in real time. And when it does, it's already old news.
I wanted a daily digest of the top Asian tech stories, translated into English, delivered automatically. So I built it.
The Stack
- Spring Boot 3 — backend pipeline
- PostgreSQL + Flyway — storage and migrations
- Google Gemini 2.5 Flash — translation, scoring, and newsletter writing
- Buttondown — newsletter delivery (free plan, full API access)
- Angular SSR — frontend
- Docker Compose on Linode — production deployment
The Pipeline
The whole system runs automatically, twice a day at 10:00 and 17:00 Shanghai time.
Step 1 — RSS Ingestion
A Spring Boot scheduler fetches RSS feeds from ~15 Chinese and Asian tech sources (ithome, huxiu, 36kr, etc.). Each article is deduplicated by URL and stored in PostgreSQL.
@Scheduled(cron = "${app.run-morning-cron}", zone = "Asia/Shanghai")
public void morningRun() {
pipeline.run();
}
Step 2 — Translation + Viral Scoring
Each new article gets sent to Gemini for:
- Translation to English (title + description)
- A viral score (0–100) based on category, topic, and potential Western interest
private static final String SYSTEM_PROMPT_SCORING = """
You are an Asian tech news editor for a Western audience.
Score each article from 0-100 based on viral potential.
Consider: AI, semiconductors, EVs, robotics = high score.
Local business news = low score.
Return ONLY a number.
""";
Category bonuses are applied on top — AI stories get +15, EV stories get +10, etc.
Step 3 — Newsletter Generation
A separate buttondown-generator service (standalone Spring Boot app) runs on its own schedule:
- Calls
GET /api/feeds/buttondown?limit=5on the main backend — returns the top 5 unprocessed stories - Sends them to Gemini to write a newsletter in HTML format
- Posts the result to Buttondown via API
- Writes the Buttondown email ID back to each article in the DB
ObjectNode body = objectMapper.createObjectNode();
body.put("subject", newsletter.getTitle());
body.put("body", newsletter.getContentHtml());
body.put("status", "about_to_send");
restClient.post()
.uri("/emails")
.header("Authorization", "Token " + config.apiKey())
.header("X-Buttondown-Live-Dangerously", "true")
.contentType(MediaType.APPLICATION_JSON)
.body(body.toString())
.retrieve()
.body(String.class);
Step 4 — The Flag System
Each article in the DB has flags per generator:
ALTER TABLE aft_feed ADD COLUMN buttondown_used BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE aft_feed ADD COLUMN youtube_used BOOLEAN NOT NULL DEFAULT FALSE;
This ensures no article is sent twice to the same channel, and each generator is fully independent. Adding a new generator (TikTok, LinkedIn, etc.) is just a new flag + a new endpoint.
The Architecture Decision — Separate Generator Services
Each generator (YouTube, Buttondown) is a separate Spring Boot app with its own Docker container, its own scheduler, and its own port.
Why not one monolith?
- Independent deployment — update the newsletter generator without touching the YouTube pipeline
- Independent failure — if Buttondown has an outage, YouTube keeps running
- Independent scaling — the YouTube generator is CPU-heavy (FFmpeg), the newsletter generator is not
- Clean separation of concerns
The main asiafeedtech-backend only exposes REST endpoints. Generators are clients.
What It Looks Like
Every morning and evening, this lands in subscribers' inboxes:
AsiafeedTech Daily — 2026-04-19
Yuanjie Technology: From 'Scammer' to A-share King
Yuanjie Technology has become China's highest-priced A-share stock, soaring 12x in a year...
This signals a shift in China's investment focus from consumer goods to high-tech hardware.
Read more →
Lessons Learned
Buttondown's "API access" on free plan is misleading. The general API is free, but the POST /emails endpoint (creating and sending posts) requires the Enterprise plan — unless you use the X-Buttondown-Live-Dangerously header, which bypasses the confirmation requirement for about_to_send status. This took a few hours to figure out.
Gemini rate limiting is real. For bulk scoring, we use a Semaphore(2) + delays + 429 detection. Flash is fast but not infinitely parallel.
Separate the @Async and @Transactional concerns in Spring Boot 3. Putting both on the same method causes proxy issues. Split into three methods: one for loading data (@Transactional(readOnly=true)), one for status updates (@Transactional), one for async work (@Async only).
What's Next
- TikTok generator — same pipeline, different output format
- Paid API access — expose the scored feed as a paid REST API for media companies and investors
- More sources — Japan, Korea, Southeast Asia
Subscribe
If you want daily Asian tech news in your inbox:
👉 buttondown.com/asiafeedtech
Free. No spam. Unsubscribe anytime.
Built with Spring Boot, Gemini AI, Buttondown, and too much coffee.