Proxies: why self-invocation does nothing

Spring Boot · lesson 19 of 39 · 4 min read

See how @Transactional and @Cacheable are applied, and the call shape that silently skips them.

Open this lesson in the learning hub

Key points

  • Spring implements @Transactional, @Cacheable and @Async with a proxy around your bean.
  • Everyone who injects that bean receives the proxy. It does its work, then calls the real object.
  • A call through this reaches the real object directly, so the proxy never runs - no transaction, no cache.
  • The same applies to private, static and final methods: a CGLIB proxy cannot override them.
  • The fix is structural. Move the annotated method into another bean and inject it.
  • Custom aspects are the same machinery: @Around advice on a pointcut, applied by the same proxy.

Example

@Service
class ReportService {

    @Transactional
    public void importAll(List<Row> rows) {
        for (Row r : rows) {
            saveOne(r);      // this.saveOne(...) - the proxy is NOT involved
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOne(Row r) { repo.save(r.toEntity()); }   // never gets its own tx
}

// Fix: the annotated method lives in another bean, so the call goes through its proxy.
@Service
class RowWriter {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOne(Row r) { repo.save(r.toEntity()); }
}

@Service
class ImportService {

    private final RowWriter writer;      // injected, therefore proxied

    ImportService(RowWriter writer) { this.writer = writer; }

    public void importAll(List<Row> rows) { rows.forEach(writer::saveOne); }
}

Spring annotations only fire on calls that arrive from outside the bean, through the proxy.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Boot course, and every lesson in it is listed on the Spring Boot contents page.