---
title: 'Transaction Management'
source: 'https://academia.sh/en/courses/enterprise-application-framework/transaction-management'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-09-13T05:08:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Runs two call paths side by side and counts how many times the declarative transaction boundary opens'
    - 'Shows that a component calling itself skips the proxy, and that the boundary never opens for the inner call'
    - 'Measures that the transaction boundary does not open even once on the same object when it is called without ever setting up a proxy'
---

# Transaction Management

Two call paths are run side by side, and how many times the declarative transaction boundary opens is counted. A component calling itself skips the proxy: the boundary never opens for the inner call, no error comes out, and this silence is marked nowhere in the source.

The previous lesson measured when an object's fields load, but it never asked where this
loading, and the changes made on top of it, begin and end. This lesson takes this up: it
measures that a declarative transaction boundary is an annotation in the source, and how a
component calling itself skips this boundary. The transaction itself — a set of changes
applying either completely or not at all, and isolation levels — was established as a
concept in the Relational Database Administration course and is not repeated here; this
lesson's question is separate from that: **where** the boundary opens and closes, and what
draws that boundary.

There is no real database connection here either. A **transaction boundary** is modeled as
the increment of a counter called `boundariesOpened` — in a real transaction, this is a
connection being put into transactional mode and then committed or rolled back at the end;
here only the question "how many times did the boundary open" is measured, not what happens
inside an opened boundary. The declarative boundary arises, with no `beginTransaction()`
call anywhere in the source, from a `@Transaction` annotation on top of a method alone.

The word "declarative" describes exactly this: the party that draws the boundary is not the
code that calls the method, it is the annotation standing on top of the method. An earlier
lesson saw this same declaration pattern in the validation rules — a field's constraint
stood in an annotation too, and the party reading it was the container. The difference in
the transaction boundary is this: whether validation applied depended only on which method
was called, while whether the transaction boundary opens depends also on **where the call
comes from**. This difference is the real subject of the rest of the lesson.

## Two Call Paths, Two Separate Boundary Counts

- **WD25.** `@Transaction` stands on both the `deposit` and `depositAndNotify` methods.
  Both are members of the same implementation class.
- **WD26.** The proxy looks up the called method again in the target object's own class
  and checks for the `@Transaction` annotation there; this is done **before** the
  `m.invoke` call, so the decision to open a boundary is made before the real call.

`AccountImplementation.depositAndNotify` calls `deposit(amount)` inside itself — this is one
method of the same object calling another of its own methods, that is, a
**self-invocation**. `Container.wrap` puts both of these methods behind a proxy, and the
proxy only sees calls coming **from outside**.

The measurement is done with two separate setups, and both use the same implementation
class. The only difference between the setups is which methods the outside caller calls and
how many times; `AccountImplementation`'s own source code stays byte-for-byte the same
across both setups. This differs in one respect from the "two setups" measurements in the
previous lessons: there, what changed was usually the scan order or the implementation
choice; here, what changes is only the pattern of calls coming from outside.

```java
// Container.java — declarative transaction boundary arises from an annotation, self-invocation skips the proxy
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

public class Container {

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
    @interface Transaction {}

    static int boundariesOpened = 0;
    static List<String> log = new ArrayList<>();

    @SuppressWarnings("unchecked")
    static <T> T wrap(T target, Class<T> iface) {
        return (T) Proxy.newProxyInstance(
                Container.class.getClassLoader(),
                new Class<?>[]{iface},
                (v, m, args) -> {
                    Method real = target.getClass().getMethod(m.getName(), m.getParameterTypes());
                    if (real.isAnnotationPresent(Transaction.class)) {
                        boundariesOpened++;
                        log.add("boundary-opened: " + m.getName());
                    }
                    try {
                        return m.invoke(target, args);
                    } catch (InvocationTargetException e) {
                        throw e.getCause();
                    }
                });
    }

    public static void main(String[] args) throws Exception {
        AccountImplementation real = new AccountImplementation();
        Account proxy = wrap(real, Account.class);

        System.out.println("-- path A: only depositAndNotify called from outside --");
        System.out.println(proxy.depositAndNotify(100));
        System.out.println("boundaries opened: " + boundariesOpened);
        System.out.println("log: " + log);

        boundariesOpened = 0;
        log.clear();
        real = new AccountImplementation();
        proxy = wrap(real, Account.class);

        System.out.println("-- path B: deposit and depositAndNotify called separately from outside --");
        System.out.println(proxy.deposit(50));
        System.out.println(proxy.depositAndNotify(100));
        System.out.println("boundaries opened: " + boundariesOpened);
        System.out.println("log: " + log);

        System.out.println("-- the inner call itself never saw the proxy --");
        System.out.println("the 'deposit(amount)' call inside AccountImplementation.depositAndNotify "
                + "is made through 'this', not through the proxy");
        System.out.println("confirmation: path B had two calls from outside, and with the inner call "
                + "deposit/depositAndNotify ran three times in total, but the boundary opened only twice");
        System.out.println("defect type: silent (no exception, the inner call ran with no boundary opened at all)");

        System.out.println("-- manually constructed object: the container is never involved --");
        AccountImplementation manual = new AccountImplementation();
        boundariesOpened = 0;
        System.out.println(manual.depositAndNotify(100));
        System.out.println("boundaries opened (proxy never constructed): " + boundariesOpened);
    }
}

interface Account {
    String deposit(int amount);
    String depositAndNotify(int amount);
}

class AccountImplementation implements Account {
    int balance = 0;

    @Container.Transaction
    public String deposit(int amount) {
        balance += amount;
        return "balance=" + balance;
    }

    @Container.Transaction
    public String depositAndNotify(int amount) {
        String result = deposit(amount);
        return result + "; notified";
    }
}
```

```
-- path A: only depositAndNotify called from outside --
balance=100; notified
boundaries opened: 1
log: [boundary-opened: depositAndNotify]
-- path B: deposit and depositAndNotify called separately from outside --
balance=50
balance=150; notified
boundaries opened: 2
log: [boundary-opened: deposit, boundary-opened: depositAndNotify]
-- the inner call itself never saw the proxy --
the 'deposit(amount)' call inside AccountImplementation.depositAndNotify is made through 'this', not through the proxy
confirmation: path B had two calls from outside, and with the inner call deposit/depositAndNotify ran three times in total, but the boundary opened only twice
defect type: silent (no exception, the inner call ran with no boundary opened at all)
-- manually constructed object: the container is never involved --
balance=100; notified
boundaries opened (proxy never constructed): 0
```

In path A there is a single outside call — `proxy.depositAndNotify(100)` — and the boundary
opens once. In path B there are two outside calls — `proxy.deposit(50)` and
`proxy.depositAndNotify(100)` — and the boundary opens twice. The only difference between the
two is how many calls come from outside; `depositAndNotify` calling `deposit` inside itself
is never added to the boundary count in either setup. This is the plainest form of what the
course calls **propagation**: the question of whether an inner call opens its own boundary
or stays inside the outer boundary — here the answer is a third possibility, the inner call
**never entering any boundary at all**, because the proxy never sees it.

## The Inner Call Never Sees the Proxy

- **WD27.** `wrap` returns only a proxy object implementing the `Account` interface;
  `AccountImplementation` itself never changes, `this` still refers to the real object.
- **WD28.** For the proxy to intercept anything, a call has to pass through the object it
  produced. The `deposit(amount)` call inside `depositAndNotify` never uses this object at
  all.

The reason for this lies in how the proxy works. `Proxy.newProxyInstance` produces a new
object — the proxy — and this object intercepts every call coming from outside, increments
`boundariesOpened`, and then forwards it to the real object (`target`). But when
`deposit(amount)` is written inside `AccountImplementation`'s own source code, this call is
not `target.deposit(amount)`, it is an ordinary Java method call made through the real
object's own `this`. The object itself never has the information "I am behind a proxy" — the
proxy is a separate layer placed around the object, not a part of the object. This is why
the `@Transaction` annotation on `deposit` is never read for this inner call: the only party
that would read it is the proxy's call handler, and the call handler only sees calls that
pass through the proxy.

This is the lesson's bounding measurement: **a component calling itself skips the proxy, and
the annotation never applies at all — because the call comes from inside, wrapping never
engages, no error comes out, no boundary opens.** Path B's log confirms this: `deposit` and
`depositAndNotify` ran three times in total (two from outside, one from inside), but the log
carries only two `boundary-opened` entries. No exception fires, no warning is printed — the
program produces a response that looks entirely normal, `balance=150; notified`. Someone
reading the source, seeing `@Transaction` on top of `deposit`, might assume it opens a
boundary every time it is called; this assumption is wrong for calls coming through
`depositAndNotify`, and the only thing that shows this wrongness is the counter, not any
error message.

The root of this behavior lies in how the proxy is set up. `Container.wrap` **produces** a
new object, it does not **modify** the existing one — not a single line was added to
`AccountImplementation`'s own bytecode, no method was replaced with an intervening call. The
proxy is a runtime-generated instance of a separate class that wraps `target`, and since this
instance implements only the `Account` interface, it is on no path at all except calls
coming through that interface. A call written inside an implementation class sees the only
object that class itself knows: itself. Being unaware of the proxy's existence is not a
shortcoming of the implementation class — the proxy is added entirely from outside, leaving
no trace on the class itself in the first place.

## The Boundary Never Opens on a Manually Constructed Object

The last measurement is made with an `AccountImplementation` object constructed without ever
calling `wrap`. The call `manual.depositAndNotify(100)` completes normally, returning
`balance=100; notified` — but `boundariesOpened` stays at zero. This is the
transaction-management counterpart of a rule seen repeatedly in the previous lessons: the
`@Transaction` annotation keeps standing on the `AccountImplementation` class, but if no
proxy was ever set up to read it, no boundary ever opens at all. The difference between the
inner call in path B and this manually constructed object is subtle but important: in the
inner call, the proxy **exists**, it does not engage for that call; in the manually
constructed object, the proxy **does not exist at all**. Both produce the same outcome — the
boundary not opening — but one comes from the object container's own rule (only calls coming
from outside get wrapped), the other comes from the container never being involved at all.

What these three setups show together is this: knowing that a declarative boundary exists
does not mean knowing that it will always run. The boundary opening depends on three
conditions all at once — the annotation has to be on the class, the call has to pass through
the proxy, and the call has to come from outside. Unless all three are met, `@Transaction`
being written in the source guarantees nothing.

None of these three conditions is written in one single place in the source text. The
`@Transaction` annotation is inside `AccountImplementation`, the proxy setup is inside
`main`, and whether a call comes from outside or from inside can only be understood by
looking at which variable the call is made through — `proxy.deposit(...)`, or the bare
`deposit(...)` that amounts to `this.deposit(...)`. Holding these three pieces of
information at once is something a code review can miss: someone looking at the body of
`depositAndNotify` can pass over which object the `deposit` call is made through without
noticing, because Java's syntax does not distinguish `deposit(...)` from `this.deposit(...)`
— they are the same call, the same appearance.

## Summary

- On the path where only `depositAndNotify` was called from outside, the boundary opened
  once; on the path where `deposit` and `depositAndNotify` were called separately from
  outside, the boundary opened twice.
- The `deposit(amount)` inner call inside `depositAndNotify` was never added to the
  boundary count in any setup; the proxy sees only calls coming from outside.
- Because the inner call was made through `this`, it never saw the proxy; even though the
  `@Transaction` annotation kept standing on the class, it was never read for this call.
- The defect was silent: the inner call threw no exception, printed no warning, it just
  completed without opening the expected boundary.
- On the same class called without ever setting up a proxy, the boundary did not open even
  once; the boundary working depends not on the annotation's existence, but on all three
  conditions being met together.

## Next Step

This topic ends here. This lesson showed that an annotation says where the boundary opens, and
that a call coming from inside never sees that boundary. The next topic asks the same
question somewhere else: in security, testing, and distributed setup, a boundary too — this
time an authorization boundary — is drawn again by an annotation, and whether a call coming
from inside sees it is measured once more.
