Hephaestus
Backend Jobs/ETL/Automation Advisor. Expert guidance on job scheduling, ETL pipelines, Groovy scripting, and automation patterns.
A Quick Note On Working with Hephaestus
Hephaestus is your Backend Jobs/ETL/Automation Advisor — expert guidance on job scheduling, ETL pipelines, Groovy scripting, and automation patterns. In practice, the work he handles is centered on backend data jobs, scheduled crons, ETL pipelines, Spring Integration flows, and Linux automation. Think data pipelines and scheduled automation — jobs that query and transform data, orchestrate document processing workflows, trigger scheduled report deliveries, and integrate with external systems.
The chat session below is a real-world example of how a conversation with Hephaestus might look — from discussing requirements, through creating a structured task file, to pair-programming the implementation top to bottom. Feel free to read through it, pick up patterns, get a sense of the rhythm.
But you don't have to follow any of it. You don't need to build payment reminders. You don't need to use pair programming. You don't need to create a task file first. Just open the chat with Hephaestus and tell him what you need — a cron job, an ETL pipeline, a data sync, a scheduled report, whatever backend automation you have in mind. Build your own way of working together. He'll adapt.
Go chat with Hephaestus. Go build stuff.
Billing Portal — Payment Reminders for Overdue Bills
Context. The user previously worked with Athena to create a Billing Portal PRD (see Athena → New Billing Portal), then worked with Hermes to build the UI/frontend app. ReportBurster handles bill generation and initial email delivery. The missing piece — automated payment reminders for overdue bills — is Hephaestus's domain.
The approach: first, the user and Hephaestus discuss requirements and create a structured
tasks file (.org). Then they implement each task one by one in a fast-paced
pair-programming session — Hephaestus guides, provides code snippets, and pushes toward
completion while the user stays in full control, understanding where each piece of code
comes from and where it goes.
Why pair programming and not a single-shot agent? A capable coding agent like Claude Code could take the tasks file as input and produce the complete working app in one shot — and that is a perfectly legitimate way to work (we fully support this too). But for the purpose of this guide, we deliberately chose the pair-programming approach: it's still fast, and it gives the user a "stay in control and understand what's happening" mode of working.
Here is the PRD document that Athena created — the starting point for this session:
Framing the Problem
Good evening! Yes, I'm here — Hephaestus, at your service.
I'm your Backend Jobs/ETL/Automation Advisor. I help you build data pipelines, scheduled jobs, Spring Integration flows, and any backend automation you need.
What's on your mind tonight?
I can see Athena's PRD — billing-portal-prd.org in her artifacts folder — solid work. She defined the data model (Customer, User, Bill), the user stories, and the ReportBurster integration.
You're right — the PRD covers bill generation and initial delivery, but there's no automated follow-up when a bill goes unpaid. That's classic backend scheduling territory, and exactly what I do.
Let's design the reminder flow together. Where would you like to start?
Great instinct — framing the problem before coding saves time. Here's what businesses typically do in the wild:
Timing
- First reminder: 1 day after due date
- Second reminder: 7 days overdue
- Third/final reminder: 14 days overdue
Why 3 reminders?
Industry standard. Enough to prompt action without spamming. After 3 unanswered reminders, most businesses escalate to a different process (collections, account suspension, phone call).
Escalation
The tone should progress:
- 1. Friendly — "just in case you missed it"
- 2. Firm — "this is now overdue, please prioritize"
- 3. Final notice — "action required within 48 hours"
Optional: Pre-due reminder
Some businesses send a courtesy reminder 3 days before the due date. Good UX, reduces overdue bills by 15-20%. Worth considering for v2.
Late fees
Common but not required for v1. If you add them later, the reminder emails just include an extra line.
Want me to draft the actual email wording for each level?
Best Practices & Email Wording
Here's a three-level approach. Every email includes: bill number, amount, due date, and a direct payment link.
Reminder 1 — Friendly (Day 1)
Subject: Friendly reminder — Bill #[number] is due
Body: "Hi [customer name], just a quick reminder that your bill #[number] for $[amount] was due on [date]. If you've already paid, please disregard this email. Otherwise, you can pay securely here: [payment link]"
Reminder 2 — Firm (Day 7)
Subject: Action needed — Bill #[number] is 7 days overdue
Body: "Dear [customer name], your bill #[number] for $[amount] is now 7 days past due (original due date: [date]). Please prioritize payment to keep your account in good standing. Pay now: [payment link]"
Reminder 3 — Final Notice (Day 14)
Subject: Final notice — Bill #[number] requires immediate attention
Body: "Dear [customer name], this is a final reminder that bill #[number] for $[amount] is 14 days overdue. Please settle this within 48 hours to avoid service disruption. Pay now: [payment link]. If you're experiencing difficulties, please contact us."
Key principles:
- Professional, never threatening
- Always include the payment link — make it easy to act
- The goal is to get paid, not to alienate customers
- Final notice mentions consequences but offers a way out ("contact us")
Technical Approach
For this use case? @Scheduled is the right call. Here's my reasoning:
What we need:
- One daily job at 9:00 AM
- Find overdue bills with reminderCount < 3
- Send the right email template based on days overdue
- Update the bill record
@Scheduled gives us:
- Zero additional dependencies (already in Spring Boot)
- Simple cron expression: @Scheduled(cron = '0 0 9 * * *')
- Runs in the application process — no external scheduler to manage
Quartz would give us:
- Persistent job store (survives restarts — but @Scheduled reruns on startup anyway)
- Distributed execution (not needed — we run one instance)
- Dynamic job creation (not needed — we have one fixed job)
- Complexity: JobDetail, Trigger, Scheduler, database tables
My recommendation:
Start with @Scheduled. It's 15 lines of code vs. 150+ for Quartz. If you ever need dynamic scheduling or clustering, we upgrade then. Don't over-engineer.
The architecture is simple:
- 1. ReminderScheduler (@Scheduled, daily 9 AM) →
- 2. OverdueBillDetector (finds overdue bills) →
- 3. ReminderService (picks template, sends email, updates bill)
Three small classes. Shall we create the task file?
Creating the Task File
Here's the task file — I'll save it as billing-portal-due-bills-reminders-tasks.org in my artifacts folder. Tasks are ordered by dependency — each one builds on the previous and delivers real value:
8 tasks. Pragmatic, ordered, each one moves us forward. Ready to start?
Pair-Programming Session
How this works. The user writes all the code — Hephaestus provides the snippets, exact file paths, and placement instructions. They work through the 8 tasks top to bottom. Watch how fast it goes once the groundwork (PRD + task file) is in place.
That's exactly how I operate — mentored pair-development. You're in the driver's seat, I navigate.
Here's the rhythm:
- 1. I give you the task + code snippet + exact file path
- 2. You paste it in
- 3. You tell me "done" or "I have a question"
- 4. We move to the next task
No detours, no over-engineering. We ship a working reminder system today.
One thing before we touch any code. Let's make a backup:
| 1 | Copy _apps/flowkraft/bkend-boot-groovy-playground |
| 2 | to _apps/flowkraft/bkend-boot-groovy-playground-Copy |
That gives us a clean reference of the original code in case we break something. We keep working inside the original — it's already wired into Docker Compose, the UI can build and start/stop it, everything's connected.
Also — my approach while we develop: we only add new code. Don't touch or delete existing classes, even if they look like stuff we don't need. They're useful as working examples while we build — you might need to reference how they handle services, injection, or configuration.
Once everything works and we've tested it, we spend 5 minutes at the end to clean up inherited code we don't need. Sound good?
Task 1 — Create the domain entities.
Two classes. First, Customer — it maps to the existing Northwind Customers table so the reminder system knows where to send emails. Create src/main/groovy/com/flowkraft/bkend/domain/Customer.groovy:
| 1 | import jakarta.persistence.* |
| 2 | |
| 3 | @Entity |
| 4 | @Table(name = "Customers") |
| 5 | class Customer { |
| 6 | |
| 7 | @Id |
| 8 | String customerId // Northwind uses string IDs (ALFKI, etc.) |
| 9 | |
| 10 | String companyName |
| 11 | String contactName |
| 12 | String email |
| 13 | String phone |
| 14 | } |
Now the Bill entity — based on Athena's PRD, plus the reminder tracking fields we discussed. Create src/main/groovy/com/flowkraft/bkend/domain/Bill.groovy:
| 1 | import jakarta.persistence.* |
| 2 | import groovy.transform.ToString |
| 3 | |
| 4 | @Entity |
| 5 | @ToString(includeNames = true, includePackage = false) |
| 6 | class Bill { |
| 7 | |
| 8 | @Id @GeneratedValue(strategy = GenerationType.IDENTITY) |
| 9 | Long id |
| 10 | |
| 11 | // Core billing fields (from Athena's PRD) |
| 12 | String billNumber |
| 13 | BigDecimal amount |
| 14 | Date dueDate |
| 15 | String pdfPath |
| 16 | |
| 17 | @ManyToOne |
| 18 | Customer customer |
| 19 | |
| 20 | // Status tracking |
| 21 | String status = 'UNPAID' // UNPAID, PAID, OVERDUE |
| 22 | |
| 23 | // Payment reminder tracking |
| 24 | Integer reminderCount = 0 |
| 25 | Date lastReminderDate |
| 26 | |
| 27 | Date dateCreated |
| 28 | Date lastUpdated |
| 29 | } |
Task 2 — Email templates. Create this folder: src/main/resources/templates/reminders/
File 1 — friendly.html:
| 1 | <html> |
| 2 | <body style="font-family: sans-serif; color: #333;"> |
| 3 | <h2>Friendly Reminder</h2> |
| 4 | <p>Hi ${customerName},</p> |
| 5 | <p>Just a quick reminder that your bill |
| 6 | <strong>#${billNumber}</strong> for |
| 7 | <strong>$${amount}</strong> was due on |
| 8 | <strong>${dueDate}</strong>.</p> |
| 9 | <p>If you've already paid, please disregard |
| 10 | this email.</p> |
| 11 | <p><a href="${paymentUrl}">Pay Now</a></p> |
| 12 | </body> |
| 13 | </html> |
File 2 — firm.html:
| 1 | <html> |
| 2 | <body style="font-family: sans-serif; color: #333;"> |
| 3 | <h2>Action Needed</h2> |
| 4 | <p>Dear ${customerName},</p> |
| 5 | <p>Your bill <strong>#${billNumber}</strong> for |
| 6 | <strong>$${amount}</strong> is now 7 days |
| 7 | past due (original due date: ${dueDate}).</p> |
| 8 | <p>Please prioritize payment to keep your |
| 9 | account in good standing.</p> |
| 10 | <p><a href="${paymentUrl}">Pay Now</a></p> |
| 11 | </body> |
| 12 | </html> |
File 3 — final-notice.html:
| 1 | <html> |
| 2 | <body style="font-family: sans-serif; color: #333;"> |
| 3 | <h2>Final Notice</h2> |
| 4 | <p>Dear ${customerName},</p> |
| 5 | <p>This is a final reminder that bill |
| 6 | <strong>#${billNumber}</strong> for |
| 7 | <strong>$${amount}</strong> is 14 days |
| 8 | overdue.</p> |
| 9 | <p>Please settle this within 48 hours to |
| 10 | avoid service disruption.</p> |
| 11 | <p><a href="${paymentUrl}">Pay Now</a></p> |
| 12 | <p style="color: #666;">If you're experiencing |
| 13 | difficulties, please contact us.</p> |
| 14 | </body> |
| 15 | </html> |
Good catch. Since we're using GStringTemplateEngine, ${paymentUrl} works directly — it binds from the model map you pass in. No escaping needed. The key is that you'll pass a Map like [customerName: bill.customer.name, billNumber: bill.billNumber, ...] when rendering the template.
Task 3 — Config properties class + application.yml.
Create src/main/groovy/com/flowkraft/bkend/reminders/ReminderConfigProperties.groovy:
| 1 | import org.springframework.boot.context.properties.ConfigurationProperties |
| 2 | import org.springframework.stereotype.Component |
| 3 | |
| 4 | @Component |
| 5 | @ConfigurationProperties(prefix = 'billing.reminders') |
| 6 | class ReminderConfigProperties { |
| 7 | List<Integer> dayThresholds = [1, 7, 14] |
| 8 | Integer maxReminders = 3 |
| 9 | String cronExpression = '0 0 9 * * *' |
| 10 | String fromAddress = '[email protected]' |
| 11 | } |
Now add this to application.yml:
| 1 | billing: |
| 2 | reminders: |
| 3 | day-thresholds: 1, 7, 14 |
| 4 | max-reminders: 3 |
| 5 | cron-expression: "0 0 9 * * *" |
| 6 | from-address: "[email protected]" |
Tasks 4 and 5 together — they're tightly coupled and both small.
First, create src/main/groovy/com/flowkraft/bkend/reminders/OverdueBillDetector.groovy:
| 1 | import org.springframework.stereotype.Service |
| 2 | |
| 3 | @Service |
| 4 | class OverdueBillDetector { |
| 5 | |
| 6 | final ReminderConfigProperties config |
| 7 | |
| 8 | OverdueBillDetector(ReminderConfigProperties config) { |
| 9 | this.config = config |
| 10 | } |
| 11 | |
| 12 | List<Bill> findBillsNeedingReminder() { |
| 13 | def today = new Date() |
| 14 | Bill.findAll( |
| 15 | "FROM Bill b WHERE b.dueDate < :today " + |
| 16 | "AND b.status != 'PAID' " + |
| 17 | "AND b.reminderCount < :max", |
| 18 | [today: today, max: config.maxReminders] |
| 19 | ) |
| 20 | } |
| 21 | } |
Now the core logic — src/main/groovy/com/flowkraft/bkend/reminders/ReminderService.groovy:
| 1 | import org.springframework.stereotype.Service |
| 2 | import org.springframework.mail.javamail.JavaMailSender |
| 3 | import org.springframework.mail.javamail.MimeMessageHelper |
| 4 | import groovy.text.GStringTemplateEngine |
| 5 | import groovy.util.logging.Slf4j |
| 6 | |
| 7 | @Slf4j |
| 8 | @Service |
| 9 | class ReminderService { |
| 10 | |
| 11 | final OverdueBillDetector detector |
| 12 | final ReminderConfigProperties config |
| 13 | final JavaMailSender mailSender |
| 14 | |
| 15 | ReminderService(OverdueBillDetector detector, |
| 16 | ReminderConfigProperties config, |
| 17 | JavaMailSender mailSender) { |
| 18 | this.detector = detector |
| 19 | this.config = config |
| 20 | this.mailSender = mailSender |
| 21 | } |
| 22 | |
| 23 | void processReminders() { |
| 24 | def overdueBills = detector.findBillsNeedingReminder() |
| 25 | overdueBills.each { bill -> |
| 26 | def templateName = pickTemplate(bill.reminderCount) |
| 27 | def html = renderTemplate(templateName, bill) |
| 28 | sendEmail(bill.customer.email, |
| 29 | subjectFor(bill.reminderCount, bill), html) |
| 30 | bill.reminderCount++ |
| 31 | bill.lastReminderDate = new Date() |
| 32 | bill.status = 'OVERDUE' |
| 33 | bill.save(flush: true) |
| 34 | } |
| 35 | log.info("Processed {} reminder(s)", overdueBills.size()) |
| 36 | } |
| 37 | |
| 38 | private String pickTemplate(int count) { |
| 39 | switch(count) { |
| 40 | case 0: return 'friendly' |
| 41 | case 1: return 'firm' |
| 42 | default: return 'final-notice' |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | private String subjectFor(int count, Bill bill) { |
| 47 | switch(count) { |
| 48 | case 0: |
| 49 | return "Friendly reminder — Bill " + |
| 50 | "#${bill.billNumber} is due" |
| 51 | case 1: |
| 52 | return "Action needed — Bill " + |
| 53 | "#${bill.billNumber} is 7 days overdue" |
| 54 | default: |
| 55 | return "Final notice — Bill " + |
| 56 | "#${bill.billNumber} requires attention" |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | private String renderTemplate(String name, Bill bill) { |
| 61 | def tpl = new GStringTemplateEngine() |
| 62 | .createTemplate( |
| 63 | getClass().getResource( |
| 64 | "/templates/reminders/${name}.html")) |
| 65 | tpl.make([ |
| 66 | customerName: bill.customer.name, |
| 67 | billNumber: bill.billNumber, |
| 68 | amount: bill.amount, |
| 69 | dueDate: bill.dueDate.format('yyyy-MM-dd'), |
| 70 | paymentUrl: "/pay/${bill.id}" |
| 71 | ]).toString() |
| 72 | } |
| 73 | |
| 74 | private void sendEmail(String to, String subject, |
| 75 | String html) { |
| 76 | def msg = mailSender.createMimeMessage() |
| 77 | def helper = new MimeMessageHelper(msg, true) |
| 78 | helper.setTo(to) |
| 79 | helper.setSubject(subject) |
| 80 | helper.setFrom(config.fromAddress) |
| 81 | helper.setText(html, true) |
| 82 | mailSender.send(msg) |
| 83 | } |
| 84 | } |
Check if there's an existing service — grep for "JavaMailSender" or "MailService" in the project. If one exists, reuse it. If not, inject JavaMailSender directly — it's already auto-configured by Spring Boot when you have spring-boot-starter-mail on the classpath.
Task 6 — the scheduler. This ties everything together.
Create src/main/groovy/com/flowkraft/bkend/reminders/ReminderScheduler.groovy:
| 1 | import org.springframework.scheduling.annotation.Scheduled |
| 2 | import org.springframework.stereotype.Component |
| 3 | import groovy.util.logging.Slf4j |
| 4 | |
| 5 | @Slf4j |
| 6 | @Component |
| 7 | class ReminderScheduler { |
| 8 | |
| 9 | final ReminderService reminderService |
| 10 | |
| 11 | ReminderScheduler(ReminderService reminderService) { |
| 12 | this.reminderService = reminderService |
| 13 | } |
| 14 | |
| 15 | @Scheduled(cron = '${billing.reminders.cron-expression}') |
| 16 | void runDailyReminders() { |
| 17 | log.info('Starting daily reminder check...') |
| 18 | reminderService.processReminders() |
| 19 | log.info('Daily reminder check complete.') |
| 20 | } |
| 21 | } |
Also — make sure your main application class has @EnableScheduling:
| 1 | @EnableScheduling |
| 2 | @SpringBootApplication |
| 3 | class Application { |
| 4 | static void main(String[] args) { |
| 5 | SpringApplication.run(Application, args) |
| 6 | } |
| 7 | } |
That's the beauty of @Scheduled. All the logic lives in the services, the scheduler just orchestrates. Clean separation.
Task 7 — config is already done (we did it in Task 3). So straight to Task 8 — smoke test.
Open your database console or create a quick script. Insert a test bill with a due date 2 days ago:
| 1 | // Quick Groovy script or SQL |
| 2 | def customer = Customer.findByEmail('[email protected]') |
| 3 | new Bill( |
| 4 | customer: customer, |
| 5 | billNumber: 'TEST-001', |
| 6 | amount: 250.00, |
| 7 | dueDate: new Date() - 2, // 2 days ago |
| 8 | status: 'UNPAID', |
| 9 | pdfPath: '/bills/test-001.pdf' |
| 10 | ).save(flush: true) |
Now either wait for the 9 AM cron or trigger it manually. For testing, temporarily change the cron to run every minute:
| 1 | billing: |
| 2 | reminders: |
| 3 | cron-expression: "0 * * * * *" # every minute (testing only!) |
Start the app and watch the logs.
Ship it first, polish later. The templates are just HTML files — you or a designer can refine them anytime without touching any Java/Groovy code.
Check the database — bill.reminderCount should be 1 now, and lastReminderDate should be today.
Change the cron back to '0 0 9 * * *' before you forget.
All tasks complete. The whole reminder system is maybe 200 lines of new code across 6 files. Here's what you built today:
Daily at 9:00 AM:
ReminderScheduler → OverdueBillDetector (finds unpaid bills past due) → ReminderService (picks template by escalation level, sends email, updates tracking)
3-level escalation: friendly (day 1) → firm (day 7) → final notice (day 14)
Everything configurable: change thresholds, timing, max reminders, or from-address in application.yml — zero code changes.
Next time you need a 4th reminder level or want to add the pre-due courtesy email, it's just a new template + a config change.
Good call — that's Hermes' domain. He can query reminderCount and lastReminderDate directly from the Bill entity you just updated. Have a good evening!
Cleaning Session
What happens here. With the reminder system working end-to-end, the user and Hephaestus review the project to verify every remaining file has a purpose — removing leftover template artifacts, consolidating configuration, and confirming the app compiles and runs as a slim, focused billing reminder service.
Right. The project has sample classes we're not using now that our own code is in place:
- SampleJob.groovy (we have our own ReminderScheduler)
- SampleService.groovy (we have our own services)
- SampleController.groovy (no web endpoints needed for this flow)
Keep anything related to Spring Boot configuration, mail setup, or database — that's infrastructure we're using.
Good instinct. Let's audit the project. Here's every file outside our new billing code, categorized:
DELETE — not needed:
HELP.md— auto-generated Spring Boot docs, not part of our app
REVIEW & UPDATE:
application.properties— has the original SQLite + Liquibase config. Our newapplication.ymladds the reminder settings. Spring Boot loads both — verify the datasource URL, Liquibase enabled/disabled, and server port are consistent between the two files.application-prod.properties— production overrides. Update for the billing reminder deployment: SMTP server, from-address, cron schedule for production.db/migration/changelog.groovy— Liquibase migration. If you're using JPA auto-DDL for development (spring.jpa.hibernate.ddl-auto=update), this file isn't active. Either update it with the Customer/Bill table definitions for production, or remove it if you're managing schema through JPA.BkendApplication.groovy— verify it has@EnableScheduling. Without it, the @Scheduled cron won't fire.BkendApplicationTests.groovy— basic context load test. Make sure it still passes with the new beans. Run./mvnw test.
KEEP — all good:
- pom.xml, Dockerfile, docker-compose.yml — infrastructure
- .gitignore, .gitattributes, .mvn/, mvnw, mvnw.cmd — tooling
- All files under domain/, reminders/, templates/ — our billing code
That's a production-ready project. Here's the final file tree:
| 1 | bkend-boot-groovy-playground/ |
| 2 | ├── pom.xml |
| 3 | ├── Dockerfile |
| 4 | ├── docker-compose.yml |
| 5 | ├── src/main/groovy/com/flowkraft/bkend/ |
| 6 | │ ├── BkendApplication.groovy # @EnableScheduling |
| 7 | │ ├── domain/ |
| 8 | │ │ ├── Customer.groovy # Northwind customer entity |
| 9 | │ │ └── Bill.groovy # Bill + reminder tracking |
| 10 | │ └── reminders/ |
| 11 | │ ├── ReminderConfigProperties.groovy |
| 12 | │ ├── OverdueBillDetector.groovy |
| 13 | │ ├── ReminderService.groovy |
| 14 | │ └── ReminderScheduler.groovy # @Scheduled daily 9 AM |
| 15 | ├── src/main/resources/ |
| 16 | │ ├── application.properties # datasource + server config |
| 17 | │ ├── application.yml # reminder settings |
| 18 | │ ├── application-prod.properties # production overrides |
| 19 | │ └── templates/reminders/ |
| 20 | │ ├── friendly.html |
| 21 | │ ├── firm.html |
| 22 | │ └── final-notice.html |
| 23 | └── src/test/groovy/.../ |
| 24 | └── BkendApplicationTests.groovy |
Slim, focused, no dead code. Every file has a purpose.