How to Eliminate Repetitive Java Code with Design Patterns, Annotations, and Bean Mapping
This article examines why duplicate code harms maintainability, then demonstrates three practical techniques—using factory and template method patterns, leveraging custom annotations with reflection, and applying bean‑copy utilities—to refactor Java business logic and dramatically reduce redundancy.
Hello everyone, I’m your friend Architect Jun, a software architect who can write code and compose poetry.
The biggest difference between software engineers and ordinary programmers is coding habits: programmers often write repetitive code, while engineers use various techniques to eliminate redundant duplication.
Business developers sometimes complain that their work lacks technical depth, claiming they never use design patterns, advanced Java features, or OOP, and that they only write CRUD code. In reality, design patterns and advanced features such as reflection, annotations, and generics are accumulated wisdom from large‑scale projects that improve maintainability. Reducing duplicate code is a key way to raise a project's maturity.
1. Using Factory Pattern + Template Method to Remove if…else and Duplicate Code
Consider a shopping‑cart order process that must handle three user types:
Normal users pay a 10% delivery fee and receive no discount.
VIP users also pay 10% delivery fee, but receive a discount when buying more than two identical items.
Internal users enjoy free delivery and no discount.
The goal is to convert an input Map<Long, Integer> (product ID → quantity) into a Cart object.
@Data
public class Cart {
private List<Item> items = new ArrayList<>();
private BigDecimal totalDiscount;
private BigDecimal totalItemPrice;
private BigDecimal totalDeliveryPrice;
private BigDecimal payPrice;
}
@Data
public class Item {
private long id;
private int quantity;
private BigDecimal price;
private BigDecimal couponPrice;
private BigDecimal deliveryPrice;
}
public class NormalUserCart {
public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();
List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);
// delivery = 10% of item total, no discount
itemList.stream().forEach(item -> {
item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
item.setCouponPrice(BigDecimal.ZERO);
});
cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}
}
public class VipUserCart {
public Cart process(long userId, Map<Long, Integer> items) {
// same initialization as NormalUserCart …
itemList.stream().forEach(item -> {
item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId))).divide(new BigDecimal("100"))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
});
// the rest of the logic is identical to NormalUserCart
...
return cart;
}
}
public class InternalUserCart {
public Cart process(long userId, Map<Long, Integer> items) {
// same initialization …
itemList.stream().forEach(item -> {
item.setDeliveryPrice(BigDecimal.ZERO);
item.setCouponPrice(BigDecimal.ZERO);
});
...
return cart;
}
}Analyzing the three implementations shows that about 70 % of the code is duplicated. The common steps—initializing the cart, converting the map to Item objects, calculating totals, and computing the final payable amount—are identical. To avoid duplication, we extract these steps into an abstract class and let subclasses implement only the variable parts.
public abstract class AbstractCart {
// common processing logic
public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();
List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);
// delegate variable logic to subclasses
itemList.forEach(item -> {
processCouponPrice(userId, item);
processDeliveryPrice(userId, item);
});
cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}
protected abstract void processCouponPrice(long userId, Item item);
protected abstract void processDeliveryPrice(long userId, Item item);
}Subclasses become concise:
@Service("NormalUserCart")
public class NormalUserCart extends AbstractCart {
@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}
@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
}
}
@Service("VipUserCart")
public class VipUserCart extends NormalUserCart {
@Override
protected void processCouponPrice(long userId, Item item) {
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId))).divide(new BigDecimal("100")))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
}
}
@Service("InternalUserCart")
public class InternalUserCart extends AbstractCart {
@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}
@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(BigDecimal.ZERO);
}
}The class diagram below visualizes the inheritance relationship:
To avoid a long if…else chain when selecting the appropriate cart, we can let Spring resolve the bean by name using the user category:
@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
String userCategory = Db.getUserCategory(userId);
AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
return cart.process(userId, items);
}This demonstrates how the combination of factory pattern and template method eliminates duplicated logic while keeping the system open for extension.
2. Using Annotations + Reflection to Eliminate Repetitive Code
Imagine a bank API that requires fixed‑length, left‑ or right‑padded strings, numeric left‑padding, monetary rounding, and an MD5 signature. A naïve implementation repeats the padding, formatting, and signing logic in every method.
public static String createUser(String name, String identity, String mobile, int age) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append(String.format("%-10s", name).replace(' ', '_'));
sb.append(String.format("%-18s", identity).replace(' ', '_'));
sb.append(String.format("%05d", age));
sb.append(String.format("%-11s", mobile).replace(' ', '_'));
sb.append(DigestUtils.md2Hex(sb.toString()));
return Request.Post("http://localhost:45678/reflection/bank/createUser")
.bodyString(sb.toString(), ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
}
public static String pay(long userId, BigDecimal amount) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append(String.format("%020d", userId));
sb.append(String.format("%010d", amount.setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
sb.append(DigestUtils.md2Hex(sb.toString()));
return Request.Post("http://localhost:45678/reflection/bank/pay")
.bodyString(sb.toString(), ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
}Both methods share the same steps: retrieve field values, apply type‑specific padding, compute MD5, and send the HTTP request. To centralize this logic, we define two custom annotations.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface BankAPI {
String desc() default "";
String url() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface BankAPIField {
int order() default -1;
int length() default -1;
String type() default ""; // S = string, N = numeric, M = monetary
}We then annotate POJOs that describe each API:
@BankAPI(url = "/bank/createUser", desc = "Create User API")
@Data
public class CreateUserAPI extends AbstractAPI {
@BankAPIField(order = 1, type = "S", length = 10)
private String name;
@BankAPIField(order = 2, type = "S", length = 18)
private String identity;
@BankAPIField(order = 3, type = "N", length = 5)
private int age;
@BankAPIField(order = 4, type = "S", length = 11)
private String mobile;
}
@BankAPI(url = "/bank/pay", desc = "Pay API")
@Data
public class PayAPI extends AbstractAPI {
@BankAPIField(order = 1, type = "N", length = 20)
private long userId;
@BankAPIField(order = 2, type = "M", length = 10)
private BigDecimal amount;
}The core reflective method processes any annotated API object:
private static String remoteCall(AbstractAPI api) throws IOException {
BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class);
StringBuilder sb = new StringBuilder();
Arrays.stream(api.getClass().getDeclaredFields())
.filter(f -> f.isAnnotationPresent(BankAPIField.class))
.sorted(Comparator.comparingInt(f -> f.getAnnotation(BankAPIField.class).order()))
.peek(f -> f.setAccessible(true))
.forEach(f -> {
BankAPIField fieldAnno = f.getAnnotation(BankAPIField.class);
Object value = "";
try { value = f.get(api); } catch (IllegalAccessException e) { e.printStackTrace(); }
switch (fieldAnno.type()) {
case "S":
sb.append(String.format("%-" + fieldAnno.length() + "s", value.toString()).replace(' ', '_'));
break;
case "N":
sb.append(String.format("%" + fieldAnno.length() + "s", value.toString()).replace(' ', '0'));
break;
case "M":
if (!(value instanceof BigDecimal)) {
throw new RuntimeException(String.format("%s field %s must be BigDecimal", api, f));
}
sb.append(String.format("%0" + fieldAnno.length() + "d",
((BigDecimal) value).setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
break;
default:
break;
}
});
sb.append(DigestUtils.md2Hex(sb.toString()));
String param = sb.toString();
long begin = System.currentTimeMillis();
String result = Request.Post("http://localhost:45678/reflection" + bankAPI.url())
.bodyString(param, ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
log.info("Calling bank API {} url:{} param:{} cost:{}ms", bankAPI.desc(), bankAPI.url(), param, System.currentTimeMillis() - begin);
return result;
}Service methods now only assemble the POJO and delegate to remoteCall:
public static String createUser(String name, String identity, String mobile, int age) throws IOException {
CreateUserAPI api = new CreateUserAPI();
api.setName(name);
api.setIdentity(identity);
api.setAge(age);
api.setMobile(mobile);
return remoteCall(api);
}
public static String pay(long userId, BigDecimal amount) throws IOException {
PayAPI api = new PayAPI();
api.setUserId(userId);
api.setAmount(amount);
return remoteCall(api);
}This approach centralizes all padding, formatting, signing, and HTTP‑call logic, eliminating the repetitive boilerplate across each API method.
3. Using Property‑Copy Tools to Eliminate Repetitive Code
In three‑tier architectures, developers often write manual field‑by‑field assignments between DTOs, DOs, and VOs. A real‑world example shows many copy‑and‑paste errors: swapped fields, duplicated assignments, and even assigning a value from the target object itself.
ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
orderDO.setAcceptDate(orderDTO.getAcceptDate());
orderDO.setAddress(orderDTO.getAddress());
orderDO.setAddressId(orderDTO.getAddressId());
orderDO.setCancelable(orderDTO.isCancelable());
orderDO.setCommentable(orderDTO.isComplainable()); // swapped
orderDO.setComplainable(orderDTO.isCommentable()); // swapped
orderDO.setCouponAmount(orderDTO.getCouponAmount());
orderDO.setDeliveryManMobile(orderDO.getDeliveryManMobile()); // self‑assignment
// … dozens of similar lines …Manually maintaining such code is error‑prone, especially when the number of fields reaches hundreds. The simple remedy is to use a bean‑mapping utility such as BeanUtils.copyProperties, optionally providing a list of fields to ignore:
ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
BeanUtils.copyProperties(orderDTO, orderDO, "id");
return orderDO;Conclusion
The article identified three common sources of code duplication and offered concrete solutions:
When multiple classes share similar business logic, extract the common part into an abstract class and apply the Template Method pattern; use Spring IoC to avoid large if…else blocks.
When the same data‑processing algorithm is hard‑coded for many APIs, describe the rules with custom annotations and let reflection handle formatting, padding, and signing in a single place.
When converting between DTO, DO, and VO objects, replace manual field assignments with a bean‑copy library and supplement with unit tests to verify correctness.
These techniques collectively improve maintainability, reduce bugs, and adhere to the Open/Closed Principle—closed for modification, open for extension.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
