How I Built a Fully Automated Asian Tech News Newsletter with Spring Boot + Gemini AI

java dev.to

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();
}
Enter fullscreen mode Exit fullscreen mode

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.
    """;
Enter fullscreen mode Exit fullscreen mode

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:

  1. Calls GET /api/feeds/buttondown?limit=5 on the main backend — returns the top 5 unprocessed stories
  2. Sends them to Gemini to write a newsletter in HTML format
  3. Posts the result to Buttondown via API
  4. 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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Source: dev.to

arrow_back Back to Tutorials