Spring anti-pattern
Spring XML and AOP hidden logic
Behavior added to beans by XML-declared advice, pointcut expressions and transaction rules, so the Java class you read is not the whole of what runs when it is called.
- FAMILY
- Spring
- CUTOVER RISK
- High
- Hides business logic or coupling that a rewrite will drop unless it is extracted explicitly.
- MODERN MAPPING
- Explicit composition with declared cross-cutting concerns
- GENERATED ARTIFACT
- advice-map.json
Before and after
legacy source → modern mapping
Legacy: exhibits the pattern
1<bean id="invoiceService"2 class="com.acme.billing.InvoiceServiceImpl">3 <property name="lateFeeRate" value="0.015"/>4</bean>5<bean id="feeCapAdvice"6 class="com.acme.billing.aop.StateFeeCapAdvice"/>78<aop:config>9 <aop:pointcut id="feeCalc" expression=10 "execution(* com.acme.billing..*Service.calculate*(..))"/>11 <aop:aspect ref="feeCapAdvice">12 <aop:around pointcut-ref="feeCalc" method="capFee"/>13 </aop:aspect>14</aop:config>1516<tx:advice id="txBilling">17 <tx:attributes>18 <tx:method name="post*" propagation="REQUIRES_NEW"19 rollback-for="BillingException"/>20 <tx:method name="*" read-only="true"/>21 </tx:attributes>22</tx:advice>
Modern: Explicit composition with declared cross-cutting concerns
1@Service2public class InvoiceService {34 private final StateFeeCap feeCap; // was: around advice5 private final BigDecimal lateFeeRate;67 public BigDecimal calculateLateFee(Invoice inv) {8 BigDecimal fee = inv.overdueAmount()9 .multiply(lateFeeRate);10 return feeCap.apply(inv.state(), fee); // explicit11 }1213 @Transactional(propagation = Propagation.REQUIRES_NEW,14 rollbackFor = BillingException.class)15 public void postLateFee(Invoice inv) {16 ledger.post(inv, calculateLateFee(inv));17 }18}
Why it is dangerous in modernization
Spring 2.x applications configured in XML often use AOP for more than logging. An around advice applied by a pointcut such as execution(* com.acme.billing..*Service.calculate*(..)) can apply a regulatory cap, check an entitlement, or swap a return value, and nothing in the target class mentions it. Transaction advice in <tx:advice> sets propagation and rollback rules per method name pattern, and those rules decide which failures undo which writes. Spring's proxy-based AOP also has a well-known gap: a call from one method to another method on the same object does not pass through the proxy, so the advice does not run. Rewrites that port the Java classes and replace XML with annotations by hand drop advice, change which methods it matches, or newly apply it to self-invocations where it never ran before.
How PYD ENTERPRISES detects it
PYD ENTERPRISES parses Spring XML contexts, including imports and ${...} property substitution, into a bean graph with concrete classes, constructor and property injection, and scopes. AspectJ pointcut expressions in <aop:config> and @Aspect classes are parsed and matched against every method of every bean, using the same matching semantics as Spring's proxy-based AOP. For each join point the engine records which advice applies, in which order, and with which transaction attributes. Intra-class calls are detected so that advice is shown as not applying on those paths. Around advice bodies are then analysed like any other code, and their decisions are extracted as rules attached to the advised methods.
Maps to: Explicit composition with declared cross-cutting concerns
Business advice becomes an explicit step in the service, called by name, with a test of its own. Genuine cross-cutting concerns such as transactions, metrics and tracing remain declarative, as Spring Boot annotations or configuration whose scope the generated report lists method by method. The goal is that a reader of the new service sees every rule that applies to it.
Generated specification fragment
1{2 "bean": "invoiceService",3 "class": "com.acme.billing.InvoiceServiceImpl",4 "joinPoints": [5 {6 "method": "calculateLateFee",7 "advice": ["feeCapAdvice.capFee (around)"],8 "tx": "read-only",9 "rule": "R-FEECAP: cap late fee by state table"10 },11 {12 "method": "postLateFee",13 "advice": [],14 "tx": "REQUIRES_NEW, rollback-for BillingException",15 "note": "calls calculateLateFee on this; proxy bypassed, fee NOT capped"16 }17 ]18}
Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.
The self-invocation finding
Look at the advice map above. External callers of calculateLateFee get the state fee cap, because their call goes through the Spring proxy. postLateFee calls calculateLateFee on this, which is the raw object, so the posted fee is uncapped. In the legacy system that is either a defect that has been overcharging customers in capped states, or an intentional difference nobody remembers. PYD ENTERPRISES cannot decide which; it reports both paths with their different results and generates a fixture for each, so the business owner decides before the rewrite makes the behavior consistent in one direction or the other.
Ordering and precedence
When several advices match one method, their order decides what each one sees. An entitlement check that runs before a caching advice behaves differently from one that runs after it. Spring resolves order from the Ordered interface, the order attribute on <aop:aspect>, or declaration order, and the engine applies the same resolution. The generated report lists the advice chain per method, in execution order, which is the information most often missing when teams move from XML to annotations.
Configuration values are rules too
lateFeeRate is set to 0.015 in XML, possibly overridden by a property substitution from an environment file. Values injected this way are business parameters. PYD ENTERPRISES resolves them per environment profile and exports them as named constants on the rules that use them, so the 1.5% rate appears in the late-fee DMN table with its source, not buried in a context file.
Related patterns
Usually found in the same codebase.
- Java EEHigh
EJB 2.x entity beans
Maps to JPA entity, repository and transactional service
- Java EEHigh
Struts god Action
Maps to Thin controller per command, domain service underneath
- PL/SQLCritical
PL/SQL trigger side effects
Maps to Explicit domain operation with events via an outbox
- SOAPMedium
SOAP and WSDL schema coupling
Maps to OpenAPI 3.1 with JSON Schema 2020-12 and problem details
Find every Spring XML and AOP hidden logic in your codebase.
Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.