Performance Benchmarking of Five Java Bean Mapping Frameworks
Layered Java applications typically use multiple object models (JPA entities, domain objects, DTOs). Moving data across these layers requires object-to-object mapping. Hand-written mappers scale poorly and are error prone, so many teams adopt mapping libraries. This document compares five popular options from a performance perspective and shows minimal setup for each.
Mapping libraries
Dozer
Dozer performs recursive, reflective property copying and supports type conversions. Configuration commonly lives in XML.
Maven dependency:
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.5.1</version>
</dependency>
Orika
Orika builds mappers that generate bytecode at runtime, reducing reflection overhead compared to pure reflection-based tools.
Maven dependency:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.2</version>
</dependency>
MapStruct
MapStruct is an annotation-driven code generator that produces mapping classes at compile time, avoiding reflection entirely while supporting custom conversions.
Maven dependencies (processor + API):
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
<scope>provided</scope>
</dependency>
ModelMapper
ModelMapper focuses on convention-based mapping, with a type-safe API and reflection under the hood. It aims for minimal configuration for typical cases.
Maven dependency:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>1.1.0</version>
</dependency>
JMapper
JMapper targets high-throughput bean mapping with a configuration model based on annotations, XML, or API definitions.
Maven dependency:
<dependency>
<groupId>com.googlecode.jmapper-framework</groupId>
<artifactId>jmapper-core</artifactId>
<version>1.6.0.1</version>
</dependency>
Test models
Two models are used to probe both trivial and realistic scenarios.
Simple source and target:
public class SourceCode {
private String code;
// getters/setters
}
public class DestinationCode {
private String code;
// getters/setters
}
Richer source and destination objects:
public class SourceOrder {
private String orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private DeliveryData deliveryData;
private User orderingUser;
private List<Product> orderedProducts;
private Shop offeringShop;
private int orderId;
private OrderStatus status;
private LocalDate orderDate;
// getters/setters
}
public class Order {
private User orderingUser;
private List<Product> orderedProducts;
private OrderStatus orderStatus;
private LocalDate orderDate;
private LocalDate orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private int shopId;
private DeliveryData deliveryData;
private Shop offeringShop;
// getters/setters
}
Converter abstraction
A minimal interface keeps the benchmark harness uniform across libraries.
public interface BeanTransformer {
Order toOrder(SourceOrder from);
DestinationCode toCode(DestinationCode from); // intentionally incorrect signature to demonstrate change? Adjust: Must be SourceCode to DestinationCode
}
Correction: Use the intended signatures.
public interface BeanTransformer {
Order toOrder(SourceOrder from);
DestinationCode toCode(SourceCode from);
}
Library-specific implementations
Orika implementation
Orika offers a fluent API to declare mappings. The example registers a property rename from status to orderStatus and relies on byDefault for the rest.
public class OrikaTransformer implements BeanTransformer {
private final MapperFacade mapper;
public OrikaTransformer() {
MapperFactory factory = new DefaultMapperFactory.Builder().build();
factory.classMap(SourceOrder.class, Order.class)
.field("status", "orderStatus")
.byDefault()
.register();
factory.classMap(SourceCode.class, DestinationCode.class).byDefault().register();
this.mapper = factory.getMapperFacade();
}
@Override
public Order toOrder(SourceOrder from) {
return mapper.map(from, Order.class);
}
@Override
public DestinationCode toCode(SourceCode from) {
return mapper.map(from, DestinationCode.class);
}
}
Dozer implementation
Dozer typically reads mappings from XML. A minimal configuration might look like this:
dozer-bean-mappings.xml
<mappings xmlns="http://dozer.sourceforge.net"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozer.sourceforge.net http://dozer.sourceforge.net/schema/beanmapping.xsd">
<mapping>
<class-a>com.example.SourceOrder</class-a>
<class-b>com.example.Order</class-b>
<field>
<a>status</a>
<b>orderStatus</b>
</field>
</mapping>
<mapping>
<class-a>com.example.SourceCode</class-a>
<class-b>com.example.DestinationCode</class-b>
</mapping>
</mappings>
Java wrapper:
public class DozerTransformer implements BeanTransformer {
private final Mapper beanMapper;
public DozerTransformer() {
DozerBeanMapper m = new DozerBeanMapper();
m.addMapping(DozerTransformer.class.getResourceAsStream("/dozer-bean-mappings.xml"));
this.beanMapper = m;
}
@Override
public Order toOrder(SourceOrder from) {
return beanMapper.map(from, Order.class);
}
@Override
public DestinationCode toCode(SourceCode from) {
return beanMapper.map(from, DestinationCode.class);
}
}
MapStruct implementation
MapStruct generates the implementation at compile time based on annotations.
@Mapper
public interface MapStructTransformer extends BeanTransformer {
MapStructTransformer INSTANCE = Mappers.getMapper(MapStructTransformer.class);
@Override
@Mapping(source = "status", target = "orderStatus")
Order toOrder(SourceOrder from);
@Override
DestinationCode toCode(SourceCode from);
}
JMapper implementation
JMapper can be configured via API. Fields in the destination can also be annotated with @JMap. Some enum conversions may require explicit converters.
public class JMapperTransformer implements BeanTransformer {
private final JMapper<Order, SourceOrder> orderMapper;
private final JMapper<DestinationCode, SourceCode> codeMapper;
public JMapperTransformer() {
JMapperAPI orderApi = new JMapperAPI()
.add(JMapperAPI.mappedClass(Order.class));
this.orderMapper = new JMapper<>(Order.class, SourceOrder.class, orderApi);
JMapperAPI codeApi = new JMapperAPI()
.add(JMapperAPI.mappedClass(DestinationCode.class));
this.codeMapper = new JMapper<>(DestinationCode.class, SourceCode.class, codeApi);
}
@Override
public Order toOrder(SourceOrder from) {
return orderMapper.getDestination(from);
}
@Override
public DestinationCode toCode(SourceCode from) {
return codeMapper.getDestination(from);
}
}
Example of a custom enum conversion method when needed:
@JMapConversion(from = "paymentType", to = "paymentType")
public PaymentType toDestPaymentType(com.example.source.PaymentType src) {
switch (src) {
case CARD: return PaymentType.CARD;
case CASH: return PaymentType.CASH;
case TRANSFER: return PaymentType.TRANSFER;
default: return null;
}
}
ModelMapper implementation
ModelMapper relies on convention for most mappings.
public class ModelMapperTransformer implements BeanTransformer {
private final ModelMapper mm = new ModelMapper();
@Override
public Order toOrder(SourceOrder from) {
return mm.map(from, Order.class);
}
@Override
public DestinationCode toCode(SourceCode from) {
return mm.map(from, DestinationCode.class);
}
}
Benchmark setup
Microbenchmarks were written using JMH. Each library has its own benchmark method, and all modes below were executed:
- AverageTime: mean execution time per operation
- Throughput: operations per second
- SingleShotTime: cold, single-invocation timing
- SampleTime: sampled latency distribution (selected percentiles)
A minimal JMH scaffold for the simple mapping case:
@State(Scope.Thread)
public class SimpleMapState {
SourceCode input;
OrikaTransformer orika = new OrikaTransformer();
DozerTransformer dozer = new DozerTransformer();
MapStructTransformer mapstruct = MapStructTransformer.INSTANCE;
ModelMapperTransformer modelMapper = new ModelMapperTransformer();
JMapperTransformer jmapper = new JMapperTransformer();
@Setup(Level.Trial)
public void init() {
input = new SourceCode();
input.setCode("abc-123");
}
}
public class SimpleBench {
@Benchmark public DestinationCode orika(SimpleMapState s) { return s.orika.toCode(s.input); }
@Benchmark public DestinationCode dozer(SimpleMapState s) { return s.dozer.toCode(s.input); }
@Benchmark public DestinationCode mapstruct(SimpleMapState s) { return s.mapstruct.toCode(s.input); }
@Benchmark public DestinationCode modelmapper(SimpleMapState s) { return s.modelMapper.toCode(s.input); }
@Benchmark public DestinationCode jmapper(SimpleMapState s) { return s.jmapper.toCode(s.input); }
}
A similar fixture is used for the richer Order mapping.
Results overview
Simple model:
- AverageTime: MapStruct and JMapper produced the lowest mean latencies.
- Throughput: MapStruct led, JMapper closely followed.
- SingleShotTime: JMapper showed the strongest cold-start numbers; MapStruct lagged in this mode due to initialization effects in some setups.
- SampleTime: Percentile latencies confirmed the above trend; MapStruct and JMapper clustered tightly at low latencies.
Realistic model:
- AverageTime: MapStruct and JMapper again ranked at the top.
- Throughput: MapStruct achieved the highest ops/s, followed by JMapper.
- SingleShotTime: Results favored JMapper on first-invocation timing.
- SampleTime: Distribution patterns were consistent with the simple case, though absolute values were higher due to object complexity.
Observed ordering across both scenarios remained stable: MapStruct and JMapper performed best overall, Orika and ModelMapper trailed in the middle depending on mode, and Dozer was consistently slowest in these measruements.