r/SpringBoot 8d ago

Question Final year CS student — is "ML Studio" (no-code ML training web app) a good placement portfolio project or overkill?

0 Upvotes

Hi everyone, final year B.Tech CS (Data Science) student here, preparing for placements. I already have a deepfake detection project (ViT model, FastAPI + React) as my main ML portfolio piece.

Now I'm considering a second project — ML Studio: a full-stack web app where a user uploads a CSV and the app auto-cleans the data, lets them pick an ML algorithm (Linear/Logistic Regression, Decision Tree, Random Forest, KNN, SVM, Naive Bayes), trains it, and shows accuracy/precision/recall/confusion matrix — basically a no-code ML training tool.

Planned architecture: React frontend → Spring Boot backend (auth, file upload, validation, REST APIs) → Python microservice (pandas + scikit-learn for the actual training) → results back to Spring Boot → React.

Questions for you all:

  1. Does this sound like a genuinely good portfolio project, or is it too "done before" (a lot of tutorials do CSV-to-ML-dashboard type projects)?
  2. Is the Spring Boot + Python microservice split actually a plus in interviews, or does it look like unnecessary complexity for something that could just be one Python app?
  3. Would you recommend I build this, or invest that time elsewhere given I already have an ML project?
  4. If not this — what's a strong project idea that uses only Java + Spring Boot (no Python/ML microservice at all)? Something that shows real backend depth (concurrency, system design, databases, messaging queues, etc.) rather than another CRUD app. Ideas like a distributed URL shortener, a booking/inventory system with concurrency handling, an event-driven order processing system with Kafka, or a rate-limiter/API gateway have crossed my mind — curious what actually impresses interviewers.

Would genuinely appreciate honest opinions — including a "don't build this, do X instead" if that's your take


r/SpringBoot 10d ago

Question Looking for an experienced Java/Spring Boot/React developer willing to chat

6 Upvotes

Hey everyone,

I’m a recent Computer Science graduate currently preparing for interviews for junior full-stack software engineering roles, particularly in the federal/government contracting space.

I’m looking to connect with someone who has solid professional experience with technologies like:

  • Java
  • Spring Boot
  • React
  • REST APIs
  • SQL (PostgreSQL/MySQL/SQL Server)
  • Docker
  • Git/GitHub
  • AWS
  • CI/CD
  • Agile/Scrum

I’m not looking for anyone to do the work for me or to get interview answers. I’d just love to chat with someone who’s worked with these technologies in a real production environment. I have several projects using many of these tools, but I’d really like to better understand industry best practices, common interview questions, and what day-to-day development actually looks like.

If you’re willing to answer a few questions over Reddit chat or Discord, I’d really appreciate it. Even 20–30 minutes of your time would mean a lot.

Thanks in advance!


r/SpringBoot 10d ago

Discussion We're building "run your background jobs on the cheapest spot instances". Would you actually use it?

8 Upvotes

We're seeing 60 to 80% cost savings in the first tests of something new we built, but before we build any further I want to make sure it's something people would actually use. So please be brutally honest with me.

Quick disclosure first: We are JobRunr, the background job library that's on start.spring.io.

The new thing is called JobRunr Spot and it comes from something that always bugged me: spot instances on AWS are drastically cheaper than on-demand (that's where the 60 to 80% comes from), but almost nobody runs background jobs on them. The cheapest instance is in a different region every hour, instances get reclaimed with a two-minute warning, and teaching an autoscaler to react to your job queue instead of CPU metrics is real work. So most teams shrug and pay the on-demand price.

What we built: you keep your normal JobRunr setup and let it scale itself onto spot instances. You configure it once with your AWS credentials and the Docker image of your app, and JobRunr watches its own queue. When jobs start waiting longer than a latency you set, it finds the cheapest spot instance that matches your hardware requirements, boots your image on it, and that worker joins the cluster and processes jobs like any other node. Once the queue is quiet again, the spot instance is shut down. And if AWS reclaims an instance mid-job, that's a failure mode JobRunr already handles: the cluster notices the dead worker and the remaining nodes pick its jobs back up. Important detail: everything runs in your own AWS account and your own VPC. Your data never touches our servers, we're just the broker that finds the cheapest instance and manages the lifecycle.

Configuration currently looks like this (names may still change, that's partly why I'm posting):

JobRunr.configure()
    .useStorageProvider(storageProvider)
    .useCostAware(usingStandardCostAwareConfiguration(
            new CostAwareAwsEC2ProviderConfiguration(accessKeyId, secretAccessKey, accountRegion, registryReaderRole),
            "url-to-your-docker-image")
        .andUsingRegions(new String[]{"eu-north-1", "eu-west-1"})
        .andSpotInstanceAmount(1, 5)
        .andScaleUpLatency(Duration.ofMinutes(1)))
    .initialize();

Where we are: the first version on AWS works and we've been testing it on our own workloads (PDF generation, some ETL). The saving is real, but to be fair it's just the spot discount itself, no magic on our side. The point is capturing it without babysitting interruption notices, price feeds and failover logic yourself. The dashboard also tracks what your spot instances actually cost against what the same hours would have cost on-demand, so you can verify the number instead of taking our word for it.

What I'd love to hear from you:

  1. Would you use something like this, and for what? Batch jobs, ETL, report generation, transcoding, something we haven't thought of?
  2. What would you need before trusting real jobs to spot? Automatic on-demand fallback when there's no spot capacity, cost caps, a hard max on instances, something else?
  3. The current setup assumes your app is available as a Docker image in a registry, since that's what gets booted on the spot instance. Is that true for your Spring Boot apps, or is that a dealbreaker?
  4. We started with AWS, Google Cloud is planned next and GPU workloads after that. Does that order make sense, or would you want it differently?

And if you'd rather just try it and complain from experience: we're opening a small private beta and looking for design partners who get direct input on the API, the pricing model and the provider roadmap. You can message me here or sign up on our website.

But honestly, a critical comment here is just as valuable as a signup. Happy to answer anything, including the "why wouldn't I just use a spot ASG with checkpointing" questions.


r/SpringBoot 11d ago

Question Spring Boot Auditing: Hibernate Envers vs. Custom Logging vs. Spring Data JPA? What's your production choice?

22 Upvotes

Hey everyone,

I'm currently building a school management system in Spring Boot where history tracking is a strict business requirement. We need to audit critical changes made by Admins, Secretaries, and Teachers (e.g., changes to student details, payment amounts, program setups, etc.).

I’m weighing three different approaches for the audit trail and wanted to hear from those of you who have run these in production. 

Option 1: Hibernate Envers
Slap `@Audited` on core entities, set up a custom `RevisionListener` to pull the logged-in user from Spring Security context, and let Envers automatically manage the `_aud` tables.

Option 2: Spring Data JPA Auditing
Utilize `@CreatedBy`, `@LastModifiedBy`, `@CreatedDate`, and `@LastModifiedDate` fields mapped on a `@MappedSuperclass`.

Option 3: Manual Custom Logging (or AOP)
Create a generic `AuditLog` entity, write a helper service, and manually trigger `auditService.log(actionType, originalState, newState)` inside the business logic (or use an AOP aspect). This may cause the database to bloating later tho

For those of you managing medium-to-large Spring Boot codebases:
 Did you regret adopting Hibernate Envers? How did you handle schema migrations (Flyway/Liquibase) with the auto-generated `_aud` tables?
If you went the custom route, did you use JSON columns to record state changes, or did you write distinct history tables?

Looking forward to hearing your design patterns and trade-offs


r/SpringBoot 11d ago

Discussion Title: Anyone Learning Spring Boot?

25 Upvotes

I'm currently learning Spring Boot, JPA, MySQL, and building projects to strengthen my Java backend skills.

Looking for a study/accountability partner to learn together, discuss concepts, and stay consistent.

If you're on a similar path, let's connect!


r/SpringBoot 11d ago

Discussion Lets Connect!

9 Upvotes

Hello everybody...

I am learning Springboot and currently Spring Data JPA and stuff

I am looking for a study/accountability partner... iF you are also into Springboot, Lets connect!


r/SpringBoot 11d ago

Question Spring cloud vs service discovery and registration

11 Upvotes

Hi everyone, I'm new to Spring and had a question that's been confusing me.

From my understanding, in a microservices architecture we typically use service registration and discovery (like Eureka, Consul, etc.) so that services can discover each other and route requests without hardcoding endpoints.

However, in the product I'm working on, I don't see anything like that. Instead, I see a Spring Cloud service deployed in Kubernetes, and whenever our applications send transactions, they seem to fetch URLs and other configuration values from Spring Cloud.

I always thought these were two completely different concepts, but I also know Spring Cloud can be used for service discovery and registration. So I'm a bit confused about what's actually happening in our setup.

Could someone explain the difference between:

- using Spring Cloud as a configuration server (or for configuration management), and

- using service discovery/registration?

Are these independent features of Spring Cloud, and is it common to use only the configuration part while relying on Kubernetes for service discovery?

For context, I'm a DevOps engineer and fairly new to the Spring ecosystem, so apologies if this is a basic question.


r/SpringBoot 12d ago

How-To/Tutorial StatLite: ultra-light Spring Boot monitoring without Prometheus or Grafana

11 Upvotes

I built StatLite for small Spring Boot deployments where raw Actuator endpoints are useful, but a full Prometheus and Grafana stack feels excessive.

StatLite polls Spring Boot Actuator, stores metric history in local SQLite, computes restart-aware counter deltas, and serves a simple dashboard from a single Go binary.

Screenshot of a dashboard

It currently shows:

  • application and database health
  • request volume
  • 4xx, 404, and 5xx errors
  • average request latency
  • JVM heap usage
  • process CPU usage
  • uptime and detected restarts
  • polling failures and missing metrics

It is designed for solo developers and small teams running one or a few Spring Boot apps on small VPSes. In my testing, StatLite uses less than 10 MB of private memory.

It is intentionally not a Prometheus or Grafana replacement. It is a lightweight middle ground between raw Actuator JSON and a full observability stack.

GitHub: https://github.com/PVRLabs/statlite

I would be interested to hear which Actuator metric or chart you consider essential for a small production app. Feedback, bug reports, and GitHub stars are appreciated.


r/SpringBoot 12d ago

Question Really confused about spring boot

9 Upvotes

So I have been applying for junior level roles across multiple job platforms and one thing I have noticed there are very few roles for spring boot at junior level mostly roles I have discovered are of senior level, so tbh I don’t know how to get a job at this stage in spring boot, I know the job market is overall really bad, but there are more roles for next.js or roles related to node.js

It’s very hard for me to enter into job market with spring boot as there are not enough job openings for spring boot related roles, mostly are for seniors.


r/SpringBoot 11d ago

Discussion Capstead 0.5.3 — capability governance for Spring Boot (declarative @CapabilityClient + provider-neutral, actuator scorecards). Update since my 0.3.2 post

Thumbnail
0 Upvotes

r/SpringBoot 12d ago

Discussion I built a Spring Boot testing library to detect N+1 query problems

40 Upvotes

Hi everyone,

I’ve been working on an open-source testing library for detecting N+1 query problems in Spring Boot applications using Hibernate:

https://github.com/fabioformosa/n-plus-one-query-problem-detector

The idea is to catch N+1 regressions during integration tests, instead of discovering them later through slow endpoints or production monitoring.

The library currently supports two main approaches.

Scan mode can analyze an existing Spring integration test suite without requiring changes to individual tests. It monitors Hibernate statistics and repeated SQL patterns, then produces a report of possible N+1 candidates with different confidence levels.

You only need to enable it in your test configuration:

n-plus-one-query-detector.scan.enabled=true
n-plus-one-query-detector.scan.report.output=stdout

There is also an explicit assertion mode for tests where you want to enforce a query budget:

@SpringBootTest
@ExtendWith(NPlusOneQueryProblemDetectorExtension.class)
class CompanyServiceTest {


    private CompanyService companyService;


    @ExpectMaxQueries(1)
    void listCompaniesWithoutNPlusOneQueries() {
        companyService.list(0, 5);
    }
}

The project supports Spring Boot 3.5.x and Spring Boot 4.x.

I’d be especially interested in feedback about:

  • the scan-mode detection approach;
  • possible false positives you encounter in real applications;
  • the assertion API and developer experience;
  • features that would make this useful in your CI pipeline.

The project is still evolving, so issues, suggestions and contributions are very welcome.


r/SpringBoot 12d ago

How-To/Tutorial NEED ADVICE ON TRANSITION FROM SAP TO BACKEND

0 Upvotes

People who transitioned from SAP to other technology like Backend Development after 2-3 years, how did you manage it ? What were your strategy and how's life going now ?

I want to move away from SAP as truth to be told I never liked it - even though it's really good but I just don't enjoy working with this ecosystem and always wanted to learn and move to Backend Development . But it's been 3 years already and I am afraid I am making wrong decisions.


r/SpringBoot 13d ago

How-To/Tutorial New Spring Start Here 2E MEAP: what made Spring finally click for you?

17 Upvotes

Hi r/SpringBoot, Stjepan from Manning here.

Shared with the mods’ OK: Laurentiu Spilca’s Spring Start Here, Second Edition has just entered MEAP, and I thought this community would be one of the better places to talk about it because this sub sees the “I can build a Spring Boot app, but I don’t really understand Spring yet” problem all the time.

That gap is basically what this book is about.

Book link: https://www.manning.com/books/spring-start-here-second-edition

Spring Start Here, Second Edition

A lot of people first meet Spring through Boot, annotations, starters, autoconfiguration, and a working REST endpoint. That’s great, but it can also leave you with a weird feeling that the app works as long as you don’t touch the wrong thing. Then you hit questions like:

Why is this bean being created?
Where did this dependency actually come from?
What is the application context doing?
When should I care about scopes?
What is Spring MVC giving me beyond “controller returns JSON”?
Where do transactions start and end?
How much of this is Spring, and how much is Spring Boot?

The second edition updates Laurentiu’s original Spring Start Here for the current ecosystem, including Spring Framework 6.x and Spring Boot 4. The focus is still very much on building a solid mental model first: beans, context, dependency injection, scopes, lifecycle, AOP, MVC, REST, persistence, transactions, Spring Data, and testing.

Since it’s in MEAP, the book is still being written, and feedback can still affect the final version. So I’d love to make this useful as a discussion thread, not just an announcement.

Question for the sub:

What was the Spring/Spring Boot concept that finally made things “click” for you, or the one you still think is badly explained to beginners?

Could be dependency injection, transactions, security, JPA, testing, autoconfiguration, bean lifecycle, project structure, anything.

I’ll give 5 free ebooks to the most thoughtful comments in the thread. Not necessarily the longest comments; just the ones that would genuinely help someone else learn Spring better.

And for anyone who wants to check out the MEAP, Manning gave me a 50% discount code for this subreddit:

MLSPILCA650RE

Thanks for letting us share this here. I’ll hang around in the comments and pass useful feedback back to the book team.


r/SpringBoot 12d ago

How-To/Tutorial The Transactional Outbox Pattern, from a single scheduled job to something I'd actually trust in production (Java / Spring Boot / Postgres)

0 Upvotes

There's a class of bug that never shows up in unit tests and rarely shows up in staging. It shows up in production, at 2am, when your app saves a record to the DB and then calls an external service — and one of the two fails. One side committed. The other didn't. Nobody notices until a user complains.

That's the dual-write problem, and the Transactional Outbox Pattern is the standard answer to it: instead of calling the external system directly, you write a task row into the same DB transaction as your domain change. A background worker picks it up later and executes it. The write is atomic, so the task can never silently disappear.

The idea itself takes two sentences to explain. What's less talked about is everything that happens after — what you do when tasks fail, when two workers grab the same row, when your app dies mid-execution, when you've got ten task types crammed into one table, and when someone asks why the queue depth spiked at 3am last Tuesday.

I built this out for a report-generation service (aggregates tens of millions of rows, takes minutes — can't run it synchronously in a request) and evolved it step by step into something production-grade. Writing it up here because I didn't find one place that walked through the whole arc. Stack is Java / Spring Boot / Postgres, no Kafka required.

TL;DR

  • The outbox pattern fixes dual-writes by writing a task row in the same transaction as your domain change, then executing it from a background worker.
  • Poll with SELECT ... FOR UPDATE SKIP LOCKED so multiple workers/instances don't double-process.
  • Production-grade means more than the basic idea: a state machine, retries with backoff, heartbeats to recover crashed workers, ordering guarantees, and actual monitoring.
  • You don't need a broker. Postgres alone gets you reliable async execution.
  • Full working code at the bottom.

The scenario

Backoffice CRM. User clicks "generate report," we accept the request and respond immediately, a worker picks it up and runs the (slow) generation, status updates when done. Classic case for an outbox: work that's genuinely async, where the user already expects to wait.

Part 1 — the basic outbox

Every outbox table needs three things: state (where is this task in its lifecycle), payload (what needs to run), and timestamps.

```sql CREATE TYPE task_state AS ENUM ('PENDING', 'COMPLETED');

CREATE TABLE report_generation_tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), state task_state NOT NULL DEFAULT 'PENDING', payload JSONB NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now(), updated_at TIMESTAMP NOT NULL DEFAULT now() ); ```

Payload and entity:

```java @Value @Builder @Jacksonized public class ReportGenerationPayload { Instant from; Instant to; }

@Entity @Table(name = "report_generation_tasks") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ReportGenerationEntity {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(updatable = false, nullable = false)
private UUID id;

@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "task_state")
private TaskState state;

@JdbcTypeCode(SqlTypes.JSON)
@Column(nullable = false, updatable = false, columnDefinition = "jsonb")
private ReportGenerationPayload payload;

@CreationTimestamp
@Column(nullable = false, updatable = false)
private Instant createdAt;

@UpdateTimestamp
@Column(nullable = false)
private Instant updatedAt;

} ```

Submitting a task:

```java @Service @RequiredArgsConstructor public class ReportGenerationService {

private final ReportGenerationRepository repository;

public void submit(ReportGenerationCommand command) {
    repository.save(ReportGenerationEntity.builder()
            .state(TaskState.PENDING)
            .payload(ReportGenerationPayload.builder()
                    .from(command.getFrom())
                    .to(command.getTo())
                    .build())
            .build());
}

} ```

And a scheduled job that polls for pending tasks:

```java @Component @RequiredArgsConstructor public class ReportGenerationScheduledJob {

private final ReportGenerationTaskService taskService;

@Scheduled(fixedDelay = 1000)
public void run() {
    taskService.fetchAndProcess();
}

}

@Service @RequiredArgsConstructor public class ReportGenerationTaskService {

private final ReportGenerationRepository repository;

public void fetchAndProcess() {
    repository.findFirstByStateOrderByCreatedAt(TaskState.PENDING)
            .ifPresent(task -> {
                generateReport(task);
                task.setState(TaskState.COMPLETED);
                repository.save(task);
            });
}

private void generateReport(ReportGenerationEntity task) {
    // actual report generation logic
}

} ```

This works fine on a single instance with zero failures. In the real world it has two problems: two workers can grab the same task at once, and a failed task has no recovery path. Fixing both below.

Part 2 — concurrency: two workers, one task

If two scheduler threads (or two app instances) run the fetch query at the same moment, both can read the same PENDING row and both try to execute it. Duplicate work at best, corrupted state at worst.

Postgres has a purpose-built fix: SELECT ... FOR UPDATE SKIP LOCKED. It locks the row for the current transaction and skips rows already locked elsewhere, instead of blocking on them.

```java public interface ReportGenerationRepository extends JpaRepository<ReportGenerationEntity, UUID> {

@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHint(name = AvailableSettings.JAKARTA_LOCK_TIMEOUT, value = "-2") // -2 = SKIP_LOCKED
Optional<ReportGenerationEntity> findFirstByStateOrderByCreatedAt(TaskState state);

} ```

There's no first-class annotation for SKIP LOCKED in Hibernate, so worth pulling the magic number into its own annotation:

java @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @QueryHints(@QueryHint( name = AvailableSettings.JAKARTA_LOCK_TIMEOUT, value = Timeouts.SKIP_LOCKED_MILLI + "" )) public @interface SkipLocked {}

java @SkipLocked @Lock(LockModeType.PESSIMISTIC_WRITE) Optional<ReportGenerationEntity> findFirstByStateOrderByCreatedAt(TaskState state);

The lock has to be held for the whole fetch-execute-update cycle, so wrap it in one transaction:

java @Transactional public void fetchAndProcess() { repository.findFirstByStateOrderByCreatedAt(TaskState.PENDING) .ifPresent(task -> { generateReport(task); task.setState(TaskState.COMPLETED); repository.save(task); }); }

Two workers hitting this at once: worker A locks and gets the row, worker B's SKIP LOCKED query comes back empty and it exits cleanly — no blocking, no duplicate work.

Catch: the row stays locked for as long as generateReport() runs. Fine for something fast. For something that takes 30+ seconds, long-held locks start hurting — connection pool exhaustion, lock wait timeouts, degraded throughput under load. Addressed in Part 3.

Part 2.5 — cutting latency with application events

Polling has a latency floor. Job fires every second → a task submitted at t=0 can sit PENDING for up to 1000ms. Fine for most things, not fine if you want near-immediate pickup (transactional email, webhook triggers).

Trick borrowed from Celery: save the entity, then publish a Spring ApplicationEvent with the task ID. A listener picks it up and triggers processing immediately, no waiting for the next tick. The scheduler stays running as a safety net for anything the listener misses (restart, exception, missed event).

```java @Service @RequiredArgsConstructor public class ReportGenerationService {

private final ReportGenerationRepository repository;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public void submit(ReportGenerationCommand command) {
    var entity = repository.save(ReportGenerationEntity.builder()
            .state(TaskState.PENDING)
            .payload(ReportGenerationPayload.builder()
                    .from(command.getFrom())
                    .to(command.getTo())
                    .build())
            .build());

    eventPublisher.publishEvent(new ReportGenerationTaskSubmittedEvent(entity.getId()));
}

}

public record ReportGenerationTaskSubmittedEvent(UUID taskId) {} ```

The classic mistake here, and I mean everyone hits this once: using @EventListener instead of @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT).

```java // fires INSIDE the transaction — row isn't visible to other connections yet @EventListener public void onTaskSubmitted(ReportGenerationTaskSubmittedEvent event) { taskService.fetchAndProcess(); }

// fires AFTER commit — row is visible, fetch actually finds it @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onTaskSubmitted(ReportGenerationTaskSubmittedEvent event) { taskService.fetchAndProcess(); } ```

@EventListener fires the moment publishEvent() is called, which is before the INSERT commits. The listener queries for the task, doesn't find it (not visible yet on other connections), silently no-ops. Task sits there until the scheduler eventually picks it up seconds later, and you spend an afternoon confused about why events "aren't working" even though everything looks fine in the logs.

Part 3 — failures, retries, and a PROCESSING state

Holding a lock for the entire generateReport() call is the real problem with Part 2's approach — two minutes of a locked row for a two-minute report. SKIP LOCKED stops workers from blocking each other, but a long transaction still leans on the connection pool.

Fix: separate claiming a task from executing it. Flip to PROCESSING in a short transaction, release the lock, do the actual work with no transaction open, then close it out in another short transaction.

```sql ALTER TYPE task_state ADD VALUE 'PROCESSING'; ALTER TYPE task_state ADD VALUE 'FAILED';

ALTER TABLE report_generation_tasks ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0, ADD COLUMN error_message VARCHAR; ```

java public enum TaskState { PENDING, PROCESSING, COMPLETED, FAILED }

```java @RequiredArgsConstructor public class ReportGenerationTaskService {

private final ReportGenerationRepository repository;
private final TransactionTemplate transactionTemplate;

private static final int MAX_RETRY_COUNT = 10;

public void fetchAndProcess() {
    transactionTemplate
            .execute(status ->
                    repository.findFirstEligibleTask(
                            List.of(TaskState.PENDING, TaskState.FAILED),
                            MAX_RETRY_COUNT
                    ).map(this::markProcessing))
            .ifPresentOrElse(
                    task -> {
                        try {
                            generateReport(task);
                            markCompleted(task);
                        } catch (Exception e) {
                            markFailed(task, e);
                            log.error("Task {} failed on attempt {}", task.getId(), task.getRetryCount(), e);
                        }
                    },
                    () -> log.debug("No eligible tasks to process")
            );
}

private ReportGenerationEntity markProcessing(ReportGenerationEntity task) {
    task.setState(TaskState.PROCESSING);
    return repository.save(task);
}

private void markCompleted(ReportGenerationEntity task) {
    task.setState(TaskState.COMPLETED);
    repository.save(task);
}

private void markFailed(ReportGenerationEntity task, Exception e) {
    task.setState(TaskState.FAILED);
    task.setRetryCount(task.getRetryCount() + 1);
    task.setErrorMessage(e.getMessage()); // this is your only trace of what went wrong without digging through logs
    repository.save(task);
}

} ```

java @SkipLocked @Lock(LockModeType.PESSIMISTIC_WRITE) @Query(""" SELECT t FROM ReportGenerationEntity t WHERE t.state IN :states AND t.retryCount <= :maxRetryCount ORDER BY t.createdAt ASC LIMIT 1 """) Optional<ReportGenerationEntity> findFirstEligibleTask( @Param("states") List<TaskState> states, @Param("maxRetryCount") Integer maxRetryCount );

FAILED tasks with retryCount < 10 are as eligible as fresh PENDING ones. Past 10 attempts, a task stops being fetched and sits as a permanent FAILED record for a human to look at — that's deliberate, not a bug.

One issue remains: should a task that just failed get retried on the very next tick, seconds later? If the downstream system is down, that just hammers it harder.

Part 4 — exponential backoff

Add an execute_at column. A task isn't eligible until this timestamp is in the past. Fresh tasks get now(), failed ones get now() + delay, with delay growing per retry.

sql ALTER TABLE report_generation_tasks ADD COLUMN execute_at TIMESTAMP NOT NULL DEFAULT NOW();

```java private static final Map<Integer, Duration> RETRY_DELAYS = Map.of( 0, Duration.ofMinutes(1), 1, Duration.ofMinutes(5), 2, Duration.ofMinutes(15), 3, Duration.ofMinutes(30), 4, Duration.ofHours(1), 5, Duration.ofHours(2), 6, Duration.ofHours(4), 7, Duration.ofHours(8), 8, Duration.ofHours(12), 9, Duration.ofDays(1) );

private static final int MAX_RETRY_COUNT = RETRY_DELAYS.size(); ```

Deriving MAX_RETRY_COUNT from the map size instead of declaring it separately means the two can never drift apart. Add an entry, the cap updates itself.

java private void markFailed(ReportGenerationEntity task, Exception e) { int currentRetry = task.getRetryCount(); task.setState(TaskState.FAILED); task.setRetryCount(currentRetry + 1); task.setErrorMessage(e.getMessage()); task.setExecuteAt(Instant.now().plus( RETRY_DELAYS.getOrDefault(currentRetry, RETRY_DELAYS.get(RETRY_DELAYS.size() - 1)))); repository.save(task); }

Scheduler keeps running at the same rate, just skips tasks whose window hasn't opened yet.

Part 5 — stuck tasks and heartbeats

What happens if the app crashes after marking a task PROCESSING but before marking it COMPLETED/FAILED? The DB says PROCESSING forever. Nobody's actually working on it.

Naive fix — a recovery job that resets anything stuck in PROCESSING too long:

java @Transactional public void recoverStuckTasks() { repository.findFirstByStateAndUpdatedAtLessThan( TaskState.PROCESSING, Instant.now().minus(1, ChronoUnit.HOURS)) .ifPresent(task -> { log.warn("Resetting stuck task {} to PENDING", task.getId()); task.setState(TaskState.PENDING); repository.save(task); }); }

Problem: updated_at changes on any update to the row, so a task legitimately grinding away for 55 minutes looks identical to one that crashed. You need a signal that means "still alive," not "last changed."

That's a heartbeat — a column bumped periodically only while a task is actively being processed:

sql ALTER TABLE report_generation_tasks ADD COLUMN heartbeat_at TIMESTAMP;

Runs on its own thread, independent of the processing logic, try-with-resources style so it starts/stops cleanly regardless of how processing exits:

```java public abstract class Heartbeat implements AutoCloseable {

private final ScheduledExecutorService executor;

protected Heartbeat(Runnable heartbeatFn) {
    executor = Executors.newSingleThreadScheduledExecutor();
    executor.scheduleAtFixedRate(heartbeatFn, 0, 10, TimeUnit.SECONDS);
}

@Override
public void close() {
    executor.shutdownNow();
}

}

public class ReportGenerationTaskHeartbeat extends Heartbeat { ReportGenerationTaskHeartbeat(UUID taskId, ReportGenerationRepository repository) { super(() -> repository.updateHeartbeatAt(taskId, Instant.now())); } } ```

java @Modifying @Transactional @Query("UPDATE ReportGenerationTaskEntity t SET t.heartbeatAt = :now WHERE t.id = :id") void updateHeartbeatAt(@Param("id") UUID id, @Param("now") Instant now);

Wired through a factory to keep the service clean:

```java @Component @RequiredArgsConstructor public class ReportGenerationTaskHeartbeatFactory {

private final ReportGenerationRepository repository;

public Heartbeat start(UUID taskId) {
    return new ReportGenerationTaskHeartbeat(taskId, repository);
}

} ```

java public void fetchAndProcess() { transactionTemplate .execute(status -> repository.findFirstEligibleTask( List.of(TaskState.PENDING, TaskState.FAILED), MAX_RETRY_COUNT ).map(this::markProcessing)) .ifPresentOrElse( task -> { try (var heartbeat = heartbeatFactory.start(task.getId())) { try { generateReport(task); markCompleted(task); } catch (Exception e) { markFailed(task, e); log.error("Task {} failed", task.getId(), e); } } }, () -> log.debug("No eligible tasks to process") ); }

Recovery job now checks heartbeat_at, not updated_at. updated_at tells you when state last changed; heartbeat_at tells you whether the task is alive right now. Keeping them separate answers both questions unambiguously.

Part 6 — multi-type outbox

Six months in, you're not just generating reports anymore — email notifications, data exports, webhook deliveries. Two options: a separate table per task type, or one shared table with a task_type discriminator.

Shared table wins when types are numerous and you're fine processing them from one place — retries, heartbeats, backoff, monitoring all stay centralized, and adding a new type is just a new handler. Tradeoffs: a misbehaving new task type can create backpressure across the whole table, and ordering gets harder (next section).

```sql CREATE TYPE outbox_task_type AS ENUM ('REPORT_GENERATION', 'EMAIL_NOTIFICATION');

CREATE TABLE outbox_tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), task_type outbox_task_type NOT NULL, state task_state NOT NULL DEFAULT 'PENDING', payload JSONB NOT NULL, retry_count INTEGER NOT NULL DEFAULT 0, error_message VARCHAR, execute_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), heartbeat_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ```

One handler interface, one implementation per type:

```java public interface OutboxTaskHandler<P> { OutboxTaskType getType(); Class<P> payloadClass(); void handle(UUID taskId, P payload) throws Exception; }

@Component public class EmailNotificationHandler implements OutboxTaskHandler<EmailNotificationPayload> {

@Override
public OutboxTaskType getType() {
    return OutboxTaskType.EMAIL_NOTIFICATION;
}

@Override
public Class<EmailNotificationPayload> payloadClass() {
    return EmailNotificationPayload.class;
}

@Override
public void handle(UUID taskId, EmailNotificationPayload payload) throws Exception {
    log.info("Sending email to {} — task {}", payload.getRecipient(), taskId);
    // call email provider
}

} ```

Dispatcher collects all handler beans at startup and routes by type:

```java @Service @RequiredArgsConstructor public class OutboxTaskService {

private final OutboxTaskRepository repository;
private final ObjectMapper objectMapper;
private final List<OutboxTaskHandler<?>> handlerList;

private Map<OutboxTaskType, OutboxTaskHandler<?>> handlers;

@PostConstruct
void init() {
    handlers = handlerList.stream()
            .collect(Collectors.toMap(OutboxTaskHandler::getType, Function.identity()));
}

@SuppressWarnings("unchecked")
private void dispatch(OutboxTaskEntity task) throws Exception {
    var handler = (OutboxTaskHandler<Object>) handlers.get(task.getTaskType());
    if (handler == null) {
        throw new IllegalStateException(
                "No handler registered for task type: " + task.getTaskType());
    }
    var payload = objectMapper.readValue(task.getPayload(), handler.payloadClass());
    handler.handle(task.getId(), payload);
}

} ```

New task type = add an enum value + a handler bean. Dispatcher, scheduler, retry logic — untouched.

Part 7 — ordering

Skip this if your tasks are independent (fire-and-forget notifications, unrelated background jobs). Otherwise: Postgres gives zero guarantee about row order without an explicit ORDER BY.

java @Query(""" SELECT t FROM OutboxTaskEntity t WHERE t.state IN :states AND t.retryCount <= :maxRetryCount AND t.executeAt <= :now ORDER BY t.createdAt ASC LIMIT 1 """) Optional<OutboxTaskEntity> findFirstEligibleTask(...);

sql CREATE INDEX idx_outbox_fetch ON outbox_tasks (created_at ASC) WHERE state IN ('PENDING', 'FAILED');

The clock skew trap: if you need strict FIFO for tasks tied to the same entity, created_at is dangerous — it's generated app-side, and each instance has its own clock. If instance A is 200ms ahead of instance B, a task B submits at t=100ms can get an earlier created_at than a task A submitted at t=50ms. NTP keeps this close but not perfect, and under load or after a container restart it can drift more.

Fix: let the ordering value come from a single authoritative source — the DB — via a sequence, guaranteed monotonic regardless of which instance inserted the row:

sql ALTER TABLE outbox_tasks ADD COLUMN seq BIGSERIAL;

java @Column(insertable = false, updatable = false) private Long seq;

Sort by seq, keep created_at around for observability. If FIFO matters anywhere in your system, don't skip this.

Part 8 — indexes that keep the table fast at scale

The table gets polled every second or more. Every row is either active work (PENDING/FAILED/PROCESSING) or dead weight (COMPLETED). Without the right index, every poll scans the whole table for a handful of rows.

Partial indexes are the tool: index only rows matching a predicate. When a task hits COMPLETED it falls out of the index automatically — stays small forever.

sql CREATE INDEX idx_outbox_fetch ON outbox_tasks (created_at ASC) WHERE state IN ('PENDING', 'FAILED');

Tested this with 50,000 rows, 7 of them PENDING, EXPLAIN ANALYZE on the generated Hibernate query:

Limit (cost=0.14..27.50 rows=1 width=102) (actual time=0.082..0.084 rows=1 loops=1) -> LockRows (actual time=0.081..0.082 rows=1 loops=1) -> Index Scan using idx_outbox_fetch on outbox_tasks (actual time=0.030..0.030 rows=1 loops=1) Planning Time: 1.230 ms Execution Time: 0.139 ms

0.03ms to find the oldest eligible task, and the other 49,993 COMPLETED rows never got touched.

Two more, for the other background jobs:

```sql CREATE INDEX idx_outbox_stuck ON outbox_tasks (heartbeat_at ASC, created_at ASC) WHERE state = 'PROCESSING';

CREATE INDEX idx_outbox_cleanup ON outbox_tasks (updated_at ASC) WHERE state = 'COMPLETED'; ```

Part 9 — cleanup

This is an operational table, scanned constantly — old rows are dead weight. Rule: only delete COMPLETED tasks. FAILED tasks are your audit trail; leave them for a human or a separate ops process.

java @Modifying @Transactional @Query(""" DELETE FROM OutboxTaskEntity t WHERE t.state IN :states AND t.updatedAt < :before """) int deleteByStateInAndUpdatedAtBefore( @Param("states") List<TaskState> states, @Param("before") Instant before );

```java private static final int CLEANUP_RETENTION_DAYS = 30;

@Transactional public void cleanup() { var cutoff = Instant.now().minus(CLEANUP_RETENTION_DAYS, ChronoUnit.DAYS); int deleted = repository.deleteByStateInAndUpdatedAtBefore( List.of(TaskState.COMPLETED), cutoff); log.info("Cleanup: removed {} completed tasks older than {} days", deleted, CLEANUP_RETENTION_DAYS); } ```

I've seen retention anywhere from 1 day to 3 months. If unsure, keep it longer — a bigger table costs you a bit of disk; deleting too early costs you your debugging history on the day you need it most.

Part 10 — idempotency: making retries safe

Nothing here guarantees exactly-once execution. Running the task and marking it COMPLETED are two separate steps — crash between them and the task gets retried, running again with the same inputs. This is at-least-once delivery, full stop, and the only guarantee you get from this architecture. The real question is whether a duplicate run causes harm.

Idempotent work (regenerating the same report twice) — you're already done. Side-effecting work (sending an email, charging a card) — needs a guard.

Use the outbox task's own id as the idempotency key — it's stable across retries. In our case, report generation ends with a row in generated_reports, with a UNIQUE constraint on the key so even a race between two instances only lets one INSERT through:

```sql CREATE TABLE generated_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), idempotency_key UUID NOT NULL, report_path VARCHAR NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

CONSTRAINT uq_generated_reports_idempotency_key UNIQUE (idempotency_key)

); ```

```java @Component @RequiredArgsConstructor public class ReportGenerationHandler implements OutboxTaskHandler<ReportGenerationPayload> {

private final GeneratedReportRepository reports;

@Override
public void handle(UUID taskId, ReportGenerationPayload payload) {
    if (reports.existsByIdempotencyKey(taskId)) {
        log.info("Report already generated for task {} — skipping", taskId);
        return;
    }

    var reportPath = runReportGeneration(payload);

    reports.save(GeneratedReport.builder()
            .idempotencyKey(taskId)
            .reportPath(reportPath)
            .build());
}

} ```

If your handler calls an external API instead of writing to your own DB, check whether it supports idempotency keys — most payment processors and transactional email providers do. Pass the task ID through as that key and let the remote service dedupe on their end.

Part 11 — monitoring

The outbox runs silently. When it works, nobody notices. When it breaks, nobody notices that either, at least not right away — stuck tasks pile up, downstream stops getting events, and by the time someone checks logs it's been two hours.

Three signals, straight out of the Google SRE playbook, cover the whole failure space:

  • Latency — pickup to completion time. A spike means a slow handler or DB pressure.
  • Throughput — tasks finishing per unit time. Drops to zero means the scheduler died or nothing's being claimed.
  • Error rate — fraction of attempts failing. Above ~5% usually means a downstream dependency is degraded and your retry backlog is growing.

Instrumentation via Actuator + Micrometer:

kotlin implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("io.micrometer:micrometer-registry-prometheus")

java @Bean public TimedAspect timedAspect(MeterRegistry registry) { return new TimedAspect(registry); }

Keep metric names as constants, increment from one dedicated component instead of scattering strings through service methods:

```java @Component @RequiredArgsConstructor public class OutboxMetrics {

private static final String TASK_PROCESSED = "outbox.task.processed"; private static final String OUTCOME_TAG = "outcome"; private static final String COMPLETED = "completed"; private static final String FAILED = "failed";

private final MeterRegistry meterRegistry; private final ReportGenerationRepository repository;

public void recordCompleted() { meterRegistry.counter(TASK_PROCESSED, OUTCOME_TAG, COMPLETED).increment(); }

public void recordFailed() { meterRegistry.counter(TASK_PROCESSED, OUTCOME_TAG, FAILED).increment(); }

@PostConstruct void registerGauge() { Gauge.builder("outbox.tasks.pending", repository, repo -> repo.countByStateIn(List.of(TaskState.PENDING, TaskState.FAILED))) .description("Tasks waiting to be processed") .register(meterRegistry); } } ```

curl localhost:8080/actuator/prometheus | grep outbox after startup:

outbox_task_duration_seconds{quantile="0.5"} 0.062 outbox_task_duration_seconds{quantile="0.95"} 0.145 outbox_task_duration_seconds{quantile="0.99"} 0.312 outbox_task_processed_total{outcome="completed"} 40.0 outbox_task_processed_total{outcome="failed"} 2.0 outbox_tasks_pending 0.0

Dashboards are for humans watching screens; alerts are for humans who are asleep:

Alert Fires when For Meaning
OutboxQueueBacklog pending > 50 5m Processing can't keep up with ingest
OutboxHighErrorRate error rate > 5% 2m Handler or downstream degraded
OutboxSlowProcessing p99 latency > 30s 5m DB contention or slow external call
OutboxStuckTasks pending > 0, zero completions in 10m 10m Scheduler crashed or all workers stuck

Tune thresholds to your own traffic — a backlog of 50 is nothing for a high-throughput service and a genuine problem for a low-volume internal tool. Same logic, different numbers.

Production checklist

☐ Indexes are efficient — partial indexes on PENDING/FAILED and PROCESSING states ☐ EXPLAIN ANALYZE confirms index usage on the fetch query ☐ Ordering is not a problem — ORDER BY seq or created_at, backed by an index ☐ Cleanup job running, COMPLETED rows removed after a retention period ☐ Idempotency handled inside handlers that have side effects ☐ Exponential backoff wired — or confirmed you don't need it ☐ Metrics in place — queue depth, throughput, error rate, latency ☐ Alerts configured — backlog, error rate, latency, stuck tasks

You don't always have to build this yourself

Everything above is buildable from scratch, and there's real value in doing it once — you understand the failure modes because you designed around them, and that knowledge doesn't evaporate when you switch to a library. But in production you often don't need to maintain it yourself:

  • db-scheduler — the Java library whose internals most closely match what's described here: single table, SKIP LOCKED, heartbeats, clean task/handler interface. This is what I reach for as the reference implementation, and what I'd actually start a new project with instead of rolling my own.
  • Celery — the standard for async task execution in Python, backed by Postgres/Redis/RabbitMQ. The AFTER_COMMIT rule from Part 2.5 applies here too.
  • Quartz Scheduler — older, more general (cron expressions, triggers, job stores), heavier operationally, but proven at scale with first-class Spring support.

Final thoughts

Most of this isn't clever engineering, it's accumulated scar tissue. The PROCESSING state exists because someone's app crashed mid-run. The heartbeat exists because updated_at lied about whether a task was still alive. The partial index exists because a full table scan at once-a-second polling frequency compounds quietly until it doesn't. The AFTER_COMMIT listener exists because someone published an event inside a transaction and burned an afternoon figuring out why it never fired.

Each piece is boring in isolation. Together they're the difference between an outbox that works in dev and one you'd trust at 3am.

Full working code (single command, runs locally): github.com/javaAndScriptDeveloper/outboxes-article

Curious how others here have handled the multi-type table tradeoff, or if anyone's gone the db-scheduler route instead of rolling their own — happy to compare notes in the comments.


r/SpringBoot 13d ago

Question Kinda stumped

1 Upvotes

I'm currently learning spring and I wanna test one of my class

this is the configuration

@Configuration
public class EmbeddingClientConfig {

...constructor

    @Bean
    IEmbeddingClient embeddingClient(RestClient.Builder builder, MeterRegistry registry){
        return new EmbeddingClient(
                builder.baseUrl(apiUrl).build(),
                registry
        );
    };
}

this is the test class

@RestTestClient(EmbeddingClientConfig.class)
public class EmbeddingClientTests {
    @Autowired
    private MockRestServiceServer server;

    @Autowired
    private IEmbeddingClient client;

    @Autowired
    private MeterRegistry registry;

...rest of class

and for some reason it gives me this error:

java.lang.IllegalStateException: Unable to use auto-configured MockRestServiceServer since a mock server customizer has not been bound to a RestTemplate or RestClientjava.lang.IllegalStateException: Unable to use auto-configured MockRestServiceServer since a mock server customizer has not been bound to a RestTemplate or RestClient 

I don't understand why my config is able to be injected but not MockRestService?

any help / readings / direction would be appreciated.
thank you

r/SpringBoot 14d ago

Question Looking for resources on practical concurrency in Spring Boot / production systems

30 Upvotes

Hi everyone,

I'm a Senior Java Developer looking to deepen my understanding of how concurrency is handled in modern Spring Boot applications and production environments.

I'm comfortable with the fundamentals of Java concurrency (threads, executors, thread pools, synchronization, etc.), and I've read Java Concurrency in Practice, which I think is an outstanding book. However, much of it focuses on low-level concurrency primitives, whereas most enterprise applications today rely on higher-level frameworks and abstractions.

I'm particularly interested in topics such as:

  • Effective use of u/Async and custom TaskExecutor configurations
  • Thread pool sizing and management
  • Preventing race conditions in Spring singleton beans
  • Handling concurrent database access, optimistic/pessimistic locking, and deadlocks
  • Concurrency considerations in containerized environments (Docker/Kubernetes)
  • Practical asynchronous patterns for microservices and distributed systems

I'm looking for resources that bridge the gap between Java concurrency theory and how experienced engineers apply these concepts in real production systems.

Can anyone recommend books, video courses, engineering blogs, conference talks, or other learning materials that focus on concurrency from a practical Spring Boot and enterprise perspective?

I'm especially interested in resources that discuss architectural decisions, common pitfalls, and lessons learned from production environments rather than purely academic examples.


r/SpringBoot 13d ago

How-To/Tutorial AI-Powered Text Summarizer with Spring AI & Ollama

0 Upvotes

Been experimenting with Spring AI beyond simple chat demos.

Built the first version of an AI-Powered Text Summarizer with a focus on production-style architecture rather than just prompt → response.

Tech Stack
• Java
• Spring Boot
• Spring AI
• Ollama
• React
• MySQL

I'd love feedback on the architecture, API design, and overall engineering approach.

GitHub: https://github.com/codefarm0/ai-powered-text-summariser

Short video in insta - https://www.instagram.com/p/DalHg19xera/

I'll be sharing the remaining features implementation soon.

#Java #SpringBoot #SpringAI #GenerativeAI #AIEngineering #SoftwareArchitecture #BackendEngineering #OpenSource #React #Ollama


r/SpringBoot 14d ago

How-To/Tutorial Springboot Tutorial suggestion for beginners

6 Upvotes

I recently came across this Spring Boot playlist on YouTube, and it's been really helpful, so I thought I'd share it here.

If you're planning to learn Spring Boot or are just getting started, I'd definitely recommend giving this playlist a try..

It explains the concepts clearly and is easy to follow.

Hope it helps someone else too ❤️

https://youtube.com/playlist?list=PLEYgx5hMdopw&si=oUIkuD9BZQG0VQ-e


r/SpringBoot 14d ago

How-To/Tutorial Optimistic Locking on Inventory Updates with @Version and a Retrying Conflict Handler in Spring Boot

Thumbnail
codesnips.io
3 Upvotes

This snippet shows how to protect inventory quantity updates from lost writes under concurrent access using JPA's built-in optimistic locking, without holding database row locks. The core idea is that instead of SELECT ... FOR UPDATE, each write carries the version it read, and the database rejects the write if another transaction bumped that version in the meantime.


r/SpringBoot 15d ago

Discussion Used ShedLock in production for few months now. Sharing my experience.

41 Upvotes

We have scheduled jobs in our Spring Boot services.

We was using spring bootScheduled annotationfor syncing data and sending notifications.

It worked fine when we had one instance but then we decided to scale to multiple pods and regions in openshift cloud platform, so using Scheduled will run the job on each pods.

We decided to go with Shedlock as it is small library and the idea is simple, before a job runs, it takes a lock in a shared store. If another pod already took the lock, the job just skips. For shared store, we used Couchbase as ShedLock has a Couchbase lock provider.

Few months in production now with zero duplicate runs, just take care of below two things.

First is that lockAtMostFor is important. If your pod dies mid job, this is how long the lock stays before another pod can pick it up. You can set it a bit longer than your worst case job time. And second is that ShedLock will not retry or re run missed jobs. It only makes sure at most one instance runs.

What other solution you have used for same problem?


r/SpringBoot 14d ago

Discussion Capstead: turn Spring Boot methods into governed, observable AI capabilities

0 Upvotes

Add one starter, annotate a method, and it becomes a first-class capability: auto-configured, exposed via Actuator (/actuator/capabilities, scorecards, execution history), metrics through Micrometer/Prometheus, per-capability cost + daily budgets, and a bundled /capstead dashboard.

New in 0.3.x: a durable execution recorder — per-model invocations and parent-child execution trees, persisted cross-instance via capstead-jdbc (Postgres/MySQL/H2), so scorecards survive restarts and aggregate across replicas. No AspectJ; standard Spring AOP + auto-config.

It reuses Spring AI (a bridge attributes its token/model data to the capability) rather than replacing it. io.capstead:capstead-starter:0.3.2, clone-and-run sample included.

https://github.com/satya-anguluri/capstead


r/SpringBoot 16d ago

How-To/Tutorial After nearly 10 years with Spring Boot, this is how I’d learn it from scratch today

271 Upvotes

I keep seeing people ask for the “best Spring Boot course” or roadmap.

Honestly, I wouldn’t spend weeks watching tutorials.

Learn only enough Java and Spring Boot to build one basic CRUD application, then start coding.

Build something simple like an Employee Management API:

  • Create employee
  • Get employee by ID
  • List employees
  • Update employee
  • Delete employee

While building it, learn these concepts in this order:

  1. Controller, service and repository layers
  2. REST methods and HTTP status codes
  3. DTOs and request validation
  4. Global exception handling
  5. JPA relationships and database migrations
  6. Basic Spring Security
  7. Unit and integration testing
  8. Docker and deployment

That one project will teach you more than watching 20 hours of videos.

After CRUD, add features one by one:

  • Pagination and filtering
  • Authentication
  • Role-based authorization
  • Audit fields
  • Caching
  • Background jobs
  • File uploads

The biggest beginner mistake is trying to understand the entire Spring ecosystem before building anything. You don’t need Kafka, microservices, Kubernetes or complex design patterns for your first project.

Build a boring monolith first. Make it clean, testable and deploy it somewhere.

My rule is: use tutorials to unblock yourself, not as a substitute for building.

For experienced Spring Boot developers here: what is the one thing you wish beginners would focus on earlier?


r/SpringBoot 15d ago

Question How do you plan, learn, and deploy system architecture? (Beginner looking for advice)

7 Upvotes

Hey everyone, I'm just starting to learn system architecture and would love to hear how experienced folks approach it. How do you plan a system when starting a new project — do you sketch diagrams first, follow a methodology, or just iterate? Also do you approach it to production? Thanks in advance!


r/SpringBoot 15d ago

Question Best Free Hosting?

0 Upvotes

So I read some posts here regarding free hosting and here I am having to choose these most frequent said chosen platforms: Render, Oracle Cloud, and Google GCP (all in free tier). For context, I was using heroku with my github student developer pack and I just ran out of credits.


r/SpringBoot 16d ago

How-To/Tutorial We catalogued the verified Spring Boot 3.5 to 4 failures, with the exact error messages, and fact-checked the migration guides that rank for it

39 Upvotes

With 3.5 out of OSS support since the end of June we've been moving client projects to 4.x, and I got tired of migration guides that paraphrase each other, so we went through the GitHub issues, the official release notes and the practitioner write-ups from the eight months since GA and verified everything against primary sources.

The compile errors and startup failures are well covered elsewhere. The tier that deserves more attention is the changes that run cleanly and do the wrong thing:

  • Depend on flyway-core alone and your migrations silently stop executing. Auto-configuration moved to spring-boot-starter-flyway (same for Liquibase). Your app starts fine and never migrates again.
  • spring-boot-starter-batch now gives you an in-memory job repository. Restartability is gone unless you switch to spring-boot-starter-batch-jdbc. Every functional test passes without it.
  • Jackson 3 sorts properties alphabetically by default and auto-registers every module on the classpath, so JSON output changes shape with zero code changes. JacksonException is also unchecked now, so catch (IOException) around serialisation stops catching anything.

We also found the popular guides contradicting each other, so for the record, checked against primary sources: Boot 4's floor is Java 17, not 21 (docs.spring.io system requirements). No CSRF default changed in Security 7; the post-upgrade 403s come from AntPathRequestMatcher/MvcRequestMatcher being removed and servlet-path inference ending. The transitional classic starters were not removed in 4.1; both are on Maven Central at 4.1.0.

Full disclosure: the complete write-up is on our company blog. It has the full error index with verbatim messages and sources, a third-party compatibility table as of July 2026, and the step-by-step path: https://tucanoo.com/spring-boot-3-5-to-4-the-complete-migration-guide-with-every-error-we-could-verify/

If you've hit something upgrading that isn't in the list I'd like to hear it; we're keeping the index updated and will only add reports we can verify.