🏗 Architect view

Coupling, patterns, Zones of Pain, refactoring priorities  ·  Software Architect, Principal Engineer

Generated 5 days ago
⚡ Agent trace  ·  13 steps  ·  $0.0533  ·  click to expand
search_claims(query='architecture layer pattern', category='structural')search_claims(query='dependency injection', category='structural')search_claims(query='repository service controller', category='structural')search_claims(query='import', category='structural')search_claims(query='class', category='structural')search_claims(query='module', category='structural')search_claims(query='file', category='informational')get_file_claims(file_path='src/main/java/org/joda/money/Money.java')get_file_claims(file_path='src/main/java/org/joda/money/BigMoney.java')get_dependencies(file_path='src/main/java/org/joda/money/Money.java')get_dependencies(file_path='src/main/java/org/joda/money/BigMoney.java')get_dependencies(file_path='src/main/java/org/joda/money/CurrencyUnit.java')get_dependencies(file_path='src/main/java/org/joda/money/BigMoneyProvider.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MoneyFormatter.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MoneyFormatterBuilder.java')get_dependencies(file_path='src/main/java/org/joda/money/Ser.java')get_dependencies(file_path='src/main/java/org/joda/money/CurrencyUnitDataProvider.java')get_dependencies(file_path='src/main/java/org/joda/money/DefaultCurrencyUnitDataProvider.java')get_dependencies(file_path='src/main/java/org/joda/money/format/AmountPrinterParser.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MoneyParseContext.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MoneyPrintContext.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MoneyPrinter.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MoneyParser.java')get_dependencies(file_path='src/main/java/org/joda/money/format/LiteralPrinterParser.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MultiPrinterParser.java')get_dependencies(file_path='src/main/java/org/joda/money/format/SignedPrinterParser.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MoneyAmountStyle.java')get_dependencies(file_path='src/main/java/org/joda/money/format/GroupingStyle.java')get_dependencies(file_path='src/main/java/org/joda/money/format/MoneyFormatException.java')get_dependencies(file_path='src/main/java/org/joda/money/CurrencyMismatchException.java')get_dependencies(file_path='src/main/java/org/joda/money/IllegalCurrencyException.java')get_dependencies(file_path='src/main/java/org/joda/money/MoneyUtils.java')read_source_file(file_path='src/main/java/module-info.java', start_line=1)read_source_file(file_path='src/main/java/org/joda/money/BigMoneyProvider.java', start_line=1)read_source_file(file_path='src/main/java/org/joda/money/Money.java', start_line=45)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyPrinter.java', start_line=1)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyParser.java', start_line=1)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyFormatter.java', start_line=34)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyFormatterBuilder.java', start_line=1)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyFormatterBuilder.java', start_line=380)read_source_file(file_path='src/test-whitebox/module-info.java', start_line=1)get_file_claims(file_path='src/main/java/org/joda/money/format/MoneyFormatterBuilder.java')get_file_claims(file_path='src/main/java/org/joda/money/format/MoneyFormatter.java')get_file_claims(file_path='src/main/java/org/joda/money/CurrencyUnit.java')get_file_claims(file_path='src/main/java/org/joda/money/CurrencyUnitDataProvider.java')get_file_claims(file_path='src/main/java/org/joda/money/DefaultCurrencyUnitDataProvider.java')

Architectural Analysis

Architecture Overview

Joda-Money is a library-style, layered architecture with two primary packages (org.joda.money and org.joda.money.format) and a clean separation of concerns. The core layer provides immutable value types (Money, BigMoney, CurrencyUnit) with a Decorator pattern where Money wraps BigMoney to enforce currency-scale rounding. The format layer implements a Builder pattern (MoneyFormatterBuilder) that composes MoneyPrinter/MoneyParser implementations into thread-safe MoneyFormatter instances. The system uses a Strategy pattern for currency data loading via CurrencyUnitDataProvider, and a Serialization Proxy pattern via Ser. Data flows from CurrencyUnitBigMoneyMoney, with formatting flowing through MoneyFormatterMultiPrinterParser → individual printer/parser implementations. The module system (Java 9+) exports both packages cleanly.

Zones of Pain

1. BigMoney — The Universal Coupling Hub (HIGH)

BigMoney is imported by 11 files across both packages, making it the single highest-in-degree module. Every printer/parser implementation in the format package (AmountPrinterParser, LiteralPrinterParser, MultiPrinterParser, SignedPrinterParser, MoneyPrinter, MoneyFormatter, MoneyFormatterBuilder, MoneyParseContext) directly depends on BigMoney. This is by design (it's the foundational monetary type), but it means any change to BigMoney's public API ripples through the entire codebase.

Evidence: - src/main/java/org/joda/money/BigMoney.java:45 — Core class, 83 structural claims - Imported by: AmountPrinterParser, LiteralPrinterParser, MoneyFormatter, MoneyFormatterBuilder, MoneyParseContext, MoneyPrinter, MultiPrinterParser, SignedPrinterParser (8 production files)

2. MoneyFormatterBuilder — Internal Enum Leakage (MEDIUM)

The builder contains two private enums (Singletons and SingletonPrinters) that implement MoneyPrinter and MoneyParser. While this is a valid optimization, it creates a hidden coupling: the builder must be modified to add new formatting tokens, violating the Open/Closed principle. The SingletonPrinters.LOCALIZED_SYMBOL printer has no corresponding parser (line 143-145), creating an asymmetry where a formatter with localized symbols can print but cannot parse.

Evidence: - src/main/java/org/joda/money/format/MoneyFormatterBuilder.java:143-145appendCurrencySymbolLocalized() appends printer but null as parser - src/main/java/org/joda/money/format/MoneyFormatterBuilder.java:309-390 — Private Singletons enum - src/main/java/org/joda/money/format/MoneyFormatterBuilder.java:396-408 — Private SingletonPrinters enum

3. CurrencyUnit — Static Global Registry (MEDIUM)

CurrencyUnit uses a static initializer that loads a CurrencyUnitDataProvider via reflection (system property or default). This creates a hidden dependency on the classpath and makes testing currency behavior difficult without white-box test modules. The static maps (currenciesByCode, currenciesByNumericCode, currenciesByCountry) are global mutable state, albeit populated once at class initialization.

Evidence: - src/main/java/org/joda/money/CurrencyUnit.java:50-677 — Static initializer loads provider via reflection - src/main/java/org/joda/money/CurrencyUnit.java:275-283registerCurrency mutates static maps - src/test-whitebox/module-info.java:20-28 — Separate test module needed to access internals

Coupling Analysis

Module In-Degree Out-Degree Concern Level Reasoning
BigMoney 8 0 HIGH Foundation type; every format component depends on it
Money 3 1 LOW Thin wrapper over BigMoney; low surface area
CurrencyUnit 3 0 MEDIUM Static registry pattern; testability concern
MoneyFormatter 0 3 LOW Facade over printer/parser chain
MoneyFormatterBuilder 0 3 MEDIUM Contains private enums that break Open/Closed
MoneyParseContext 0 2 LOW Mutable parse state, single-use
MoneyPrintContext 0 0 LOW Simple mutable context, no dependencies
Ser 0 0 LOW Serialization proxy, isolated
CurrencyUnitDataProvider 0 1 LOW Abstract strategy, single implementation
DefaultCurrencyUnitDataProvider 0 0 LOW Concrete strategy, file-based

Key observation: The dependency graph is a directed acyclic graph (DAG) with no cycles — a strong architectural property. The format package depends on the core package, never the reverse.

Design Pattern Inventory

Pattern Location Consistency Evidence
Decorator Money wrapping BigMoney ✅ STRONG Money.java:55private final BigMoney money; All operations delegate and wrap
Builder MoneyFormatterBuilderMoneyFormatter ✅ STRONG MoneyFormatterBuilder.java:34 — Mutable builder, toFormatter() produces immutable result
Strategy CurrencyUnitDataProvider ✅ STRONG CurrencyUnitDataProvider.java:21 — Abstract class, loaded via system property
Serialization Proxy Ser class ✅ STRONG Money.java:377-379, BigMoney.java:430-432, CurrencyUnit.java:473-475writeReplace returns Ser
Singleton (enum) Singletons, SingletonPrinters ⚠️ INCONSISTENT Private enums inside builder — works but breaks Open/Closed
Facade MoneyFormatter ✅ STRONG MoneyFormatter.java:34 — Single class hiding MultiPrinterParser chain
Composite MultiPrinterParser ✅ STRONG MultiPrinterParser.java:29 — Implements same interface as leaves, delegates to array
Template Method CurrencyUnitDataProvider.registerCurrencies() ✅ STRONG Abstract method called by static initializer, subclasses provide implementation

Broken/Inconsistent patterns: - Parser/Printer asymmetry: appendCurrencySymbolLocalized() provides a printer but no parser (MoneyFormatterBuilder.java:143-145). This means a formatter built with localized symbols can print but will fail at parse time. This is a design smell — the builder should either enforce symmetry or make the asymmetry explicit in the API. - Missing Null Object pattern: MoneyFormatterBuilder uses null for missing parsers (line 145). A NullParser singleton would be cleaner and avoid null checks throughout the chain.

Refactoring Priorities

1. Extract Printer/Parser Singletons from Builder (HIGH — 2-3 days)

Problem: MoneyFormatterBuilder.java:309-408 contains two private enums implementing MoneyPrinter/MoneyParser. This violates Open/Closed and makes the builder class ~410 lines with mixed responsibilities.

Action: Extract Singletons and SingletonPrinters into package-private top-level classes or a dedicated inner package. This allows new formatting tokens to be added without modifying the builder.

Benefit: Cleaner separation of concerns, easier testing, Open/Closed compliance.

2. Add NullParser for Symmetry (MEDIUM — 1 day)

Problem: MoneyFormatterBuilder.java:145 passes null as parser for localized symbols. This requires null checks in MultiPrinterParser and MoneyFormatter.

Action: Create a package-private NullParser implementing MoneyParser that sets an error on the context. Replace null references.

Benefit: Eliminates null checks, makes parse failures explicit and predictable.

3. Make CurrencyUnit Testable (MEDIUM — 3-5 days)

Problem: CurrencyUnit uses a static initializer with reflection-based provider loading (CurrencyUnit.java:50-677). The test-whitebox module exists specifically to work around this.

Action: Add a package-private CurrencyUnit.reset() or CurrencyUnit.reinitialize(CurrencyUnitDataProvider) method for testing. Alternatively, make the provider injectable via a builder pattern.

Benefit: Eliminates need for separate test module, enables unit testing of currency-dependent code without classpath manipulation.

4. Document Scale Semantics in Comparison Methods (LOW — 0.5 day)

Problem: Money.java:1274-1302 has @drift claims indicating that isLessThan and isLessThanOrEqual docstrings claim scale is ignored, but the actual behavior depends on BigMoney's implementation. BigMoney.equals() considers scale (line 1695-1705), while compareTo() ignores it (line 1608-1615).

Action: Add explicit documentation to Money's comparison methods clarifying that they delegate to BigMoney.compareTo() (which ignores scale) vs BigMoney.equals() (which considers scale).

Benefit: Prevents subtle bugs where developers assume equals and compareTo are consistent.

5. Consider Removing BigMoneyProvider Interface (LOW — 2 days)

Problem: BigMoneyProvider.java:22 explicitly states "In general, applications should use the concrete types, not this interface." The interface has only one method (toBigMoney()) and is implemented by both Money and BigMoney. It adds abstraction overhead without clear benefit.

Action: Evaluate if the interface is used by external consumers. If not, deprecate it and simplify the type hierarchy.

Benefit: Reduces conceptual surface area; eliminates a layer of indirection that the authors themselves advise against using.