diff --git a/README.md b/README.md
index 12a43ffee..e03eceec6 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,51 @@ You can use Maven and add this dependency to your project's POM:
Alternatively, you can download the [release on GitHub](https://github.com/Adyen/adyen-java-api-library/releases).
+### Modular artifacts
+
+The default artifact contains every API, model, webhook, and Terminal API class. Applications that
+only use part of the library can instead depend on the `core` classifier and one or more API-family
+classifiers:
+
+| Classifier | Contents |
+|---|---|
+| `core` | Client, HTTP transport, shared request and error models, serializers, and utilities |
+| `checkout` | Checkout API and models |
+| `platforms` | Balance Platform, Balance Control, Capital, Legal Entity Management, Management, Session Authentication, and Transfers APIs and models |
+| `payments` | Bin Lookup, Data Protection, Disputes, Open Banking, Payments, Payouts, Recurring, and Stored Value APIs and models |
+| `terminal` | Terminal API, Cloud Device, Mobile, Payments App, and Terminal Fleet Management classes and models |
+| `classic-platforms` | Classic Platforms APIs and models |
+| `webhooks` | Webhook models and HMAC validation |
+| `webhook-handlers` | Convenience webhook handlers; also requires `webhooks`, `payments`, and `terminal` |
+
+Every API-family classifier requires `core`. For example, a Maven application that only uses the
+Checkout API can use:
+
+```xml
+
+ com.adyen
+ adyen-java-api-library
+ 43.0.0
+ core
+
+
+ com.adyen
+ adyen-java-api-library
+ 43.0.0
+ checkout
+
+```
+
+The equivalent Gradle dependencies are:
+
+```groovy
+implementation("com.adyen:adyen-java-api-library:43.0.0:core")
+implementation("com.adyen:adyen-java-api-library:43.0.0:checkout")
+```
+
+The unclassified artifact remains available and unchanged for applications that prefer a single
+dependency or need the complete library.
+
## Using the library
### General use with API key
diff --git a/pom.xml b/pom.xml
index 69284fdfd..9e88d2c99 100644
--- a/pom.xml
+++ b/pom.xml
@@ -87,6 +87,207 @@
${project.build.outputDirectory}/META-INF/MANIFEST.MF
+
+
+ core-jar
+ package
+
+ jar
+
+
+ core
+
+ com/adyen/*.class
+ com/adyen/constants/**
+ com/adyen/enums/**
+ com/adyen/httpclient/**
+ com/adyen/model/*.class
+ com/adyen/model/checkout/JSON*.class
+ com/adyen/serializer/**
+ com/adyen/service/exception/**
+ com/adyen/service/resource/*.class
+ com/adyen/util/*.class
+
+
+ com/adyen/httpclient/TerminalLocalAPIHostnameVerifier.class
+ com/adyen/serializer/SaleToAcquirerDataSerializer.class
+ com/adyen/util/HMACValidator.class
+
+
+
+
+ checkout-jar
+ package
+
+ jar
+
+
+ checkout
+
+ com/adyen/model/checkout/**
+ com/adyen/model/checkoututility/**
+ com/adyen/service/checkout/**
+
+
+ com/adyen/model/checkout/JSON*.class
+
+
+
+
+ platforms-jar
+ package
+
+ jar
+
+
+ platforms
+
+ com/adyen/model/balancecontrol/**
+ com/adyen/model/balanceplatform/**
+ com/adyen/model/capital/**
+ com/adyen/model/legalentitymanagement/**
+ com/adyen/model/management/**
+ com/adyen/model/sessionauthentication/**
+ com/adyen/model/transfers/**
+ com/adyen/service/BalanceControlApi*.class
+ com/adyen/service/balancecontrol/**
+ com/adyen/service/balanceplatform/**
+ com/adyen/service/capital/**
+ com/adyen/service/legalentitymanagement/**
+ com/adyen/service/management/**
+ com/adyen/service/sessionauthentication/**
+ com/adyen/service/transfers/**
+
+
+
+
+ payments-jar
+ package
+
+ jar
+
+
+ payments
+
+ com/adyen/model/additionalData/**
+ com/adyen/model/binlookup/**
+ com/adyen/model/dataprotection/**
+ com/adyen/model/disputes/**
+ com/adyen/model/openbanking/**
+ com/adyen/model/payment/**
+ com/adyen/model/payout/**
+ com/adyen/model/recurring/**
+ com/adyen/model/storedvalue/**
+ com/adyen/service/BinLookupApi*.class
+ com/adyen/service/DataProtectionApi*.class
+ com/adyen/service/DisputesApi*.class
+ com/adyen/service/PaymentApi*.class
+ com/adyen/service/RecurringApi*.class
+ com/adyen/service/StoredValueApi*.class
+ com/adyen/service/binlookup/**
+ com/adyen/service/dataprotection/**
+ com/adyen/service/disputes/**
+ com/adyen/service/openbanking/**
+ com/adyen/service/payment/**
+ com/adyen/service/payout/**
+ com/adyen/service/recurring/**
+ com/adyen/service/storedvalue/**
+
+
+
+
+ terminal-jar
+ package
+
+ jar
+
+
+ terminal
+
+ com/adyen/builders/**
+ com/adyen/httpclient/TerminalLocalAPIHostnameVerifier.class
+ com/adyen/model/applicationinfo/**
+ com/adyen/model/clouddevice/**
+ com/adyen/model/nexo/**
+ com/adyen/model/paymentsapp/**
+ com/adyen/model/posmobile/**
+ com/adyen/model/posterminalmanagement/**
+ com/adyen/model/tapi/**
+ com/adyen/model/terminal/**
+ com/adyen/security/**
+ com/adyen/serializer/SaleToAcquirerDataSerializer.class
+ com/adyen/service/PaymentsAppApi*.class
+ com/adyen/service/PosMobileApi*.class
+ com/adyen/service/PosPayment*.class
+ com/adyen/service/PosTerminalManagementApi*.class
+ com/adyen/service/TerminalCloudAPI*.class
+ com/adyen/service/TerminalLocalAPI*.class
+ com/adyen/service/clouddevice/**
+ com/adyen/service/paymentsapp/**
+ com/adyen/service/posmobile/**
+ com/adyen/service/resource/terminal/**
+ com/adyen/terminal/**
+ com/adyen/util/tapi/**
+
+
+
+
+ classic-platforms-jar
+ package
+
+ jar
+
+
+ classic-platforms
+
+ com/adyen/model/marketpayaccount/**
+ com/adyen/model/marketpayconfiguration/**
+ com/adyen/model/marketpayfund/**
+ com/adyen/model/marketpayhop/**
+ com/adyen/service/classicplatforms/**
+
+
+
+
+ webhooks-jar
+ package
+
+ jar
+
+
+ webhooks
+
+ com/adyen/model/acswebhooks/**
+ com/adyen/model/balancewebhooks/**
+ com/adyen/model/configurationwebhooks/**
+ com/adyen/model/disputewebhooks/**
+ com/adyen/model/managementwebhooks/**
+ com/adyen/model/marketpaywebhooks/**
+ com/adyen/model/negativebalancewarningwebhooks/**
+ com/adyen/model/notification/**
+ com/adyen/model/relayedauthorizationwebhooks/**
+ com/adyen/model/reportwebhooks/**
+ com/adyen/model/tokenizationwebhooks/**
+ com/adyen/model/transactionwebhooks/**
+ com/adyen/model/transferwebhooks/**
+ com/adyen/util/HMACValidator.class
+
+
+
+
+ webhook-handlers-jar
+ package
+
+ jar
+
+
+ webhook-handlers
+
+ com/adyen/notification/**
+
+
+
+
org.jacoco
diff --git a/src/test/java/com/adyen/ModularArtifactsTest.java b/src/test/java/com/adyen/ModularArtifactsTest.java
new file mode 100644
index 000000000..4d16e170f
--- /dev/null
+++ b/src/test/java/com/adyen/ModularArtifactsTest.java
@@ -0,0 +1,145 @@
+package com.adyen;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+class ModularArtifactsTest {
+
+ @Test
+ void modularArtifactsPartitionAllCompiledClasses() throws Exception {
+ Map jarPatterns = readJarPatterns();
+ assertEquals(
+ Set.of(
+ "core",
+ "checkout",
+ "platforms",
+ "payments",
+ "terminal",
+ "classic-platforms",
+ "webhooks",
+ "webhook-handlers"),
+ jarPatterns.keySet());
+
+ Map> ownersByClass = new HashMap<>();
+ try (Stream compiledClasses = Files.walk(Path.of("target/classes/com/adyen"))) {
+ compiledClasses
+ .filter(path -> path.toString().endsWith(".class"))
+ .map(path -> Path.of("target/classes").relativize(path).toString().replace('\\', '/'))
+ .forEach(
+ className ->
+ ownersByClass.put(
+ className,
+ jarPatterns.entrySet().stream()
+ .filter(entry -> entry.getValue().matches(className))
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList())));
+ }
+
+ List unassigned =
+ ownersByClass.entrySet().stream()
+ .filter(entry -> entry.getValue().isEmpty())
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList());
+ List duplicated =
+ ownersByClass.entrySet().stream()
+ .filter(entry -> entry.getValue().size() > 1)
+ .map(entry -> entry.getKey() + " -> " + entry.getValue())
+ .collect(Collectors.toList());
+
+ assertTrue(unassigned.isEmpty(), "Classes missing from modular artifacts: " + unassigned);
+ assertTrue(duplicated.isEmpty(), "Classes duplicated across modular artifacts: " + duplicated);
+ }
+
+ private static Map readJarPatterns() throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ NodeList plugins =
+ factory
+ .newDocumentBuilder()
+ .parse(Path.of("pom.xml").toFile())
+ .getElementsByTagName("plugin");
+ Map patternsByClassifier = new HashMap<>();
+
+ for (int pluginIndex = 0; pluginIndex < plugins.getLength(); pluginIndex++) {
+ Element plugin = (Element) plugins.item(pluginIndex);
+ if (!"maven-jar-plugin".equals(text(plugin, "artifactId"))) {
+ continue;
+ }
+ NodeList executions = plugin.getElementsByTagName("execution");
+ for (int executionIndex = 0; executionIndex < executions.getLength(); executionIndex++) {
+ Element execution = (Element) executions.item(executionIndex);
+ NodeList classifiers = execution.getElementsByTagName("classifier");
+ if (classifiers.getLength() == 0) {
+ continue;
+ }
+ String classifier = classifiers.item(0).getTextContent().trim();
+ patternsByClassifier.put(
+ classifier,
+ new JarPatterns(patterns(execution, "include"), patterns(execution, "exclude")));
+ }
+ }
+ return patternsByClassifier;
+ }
+
+ private static String text(Element element, String tagName) {
+ return element.getElementsByTagName(tagName).item(0).getTextContent().trim();
+ }
+
+ private static List patterns(Element execution, String tagName) {
+ NodeList nodes = execution.getElementsByTagName(tagName);
+ List patterns = new ArrayList<>();
+ for (int index = 0; index < nodes.getLength(); index++) {
+ patterns.add(Pattern.compile(toRegex(nodes.item(index).getTextContent().trim())));
+ }
+ return patterns;
+ }
+
+ private static String toRegex(String glob) {
+ StringBuilder regex = new StringBuilder("^");
+ for (int index = 0; index < glob.length(); index++) {
+ char character = glob.charAt(index);
+ if (character == '*') {
+ boolean doubleWildcard = index + 1 < glob.length() && glob.charAt(index + 1) == '*';
+ regex.append(doubleWildcard ? ".*" : "[^/]*");
+ if (doubleWildcard) {
+ index++;
+ }
+ } else if ("\\.[]{}()+-^$|".indexOf(character) >= 0) {
+ regex.append('\\').append(character);
+ } else {
+ regex.append(character);
+ }
+ }
+ return regex.append('$').toString();
+ }
+
+ private static final class JarPatterns {
+ private final List includes;
+ private final List excludes;
+
+ private JarPatterns(List includes, List excludes) {
+ this.includes = includes;
+ this.excludes = excludes;
+ }
+
+ private boolean matches(String className) {
+ return includes.stream().anyMatch(pattern -> pattern.matcher(className).matches())
+ && excludes.stream().noneMatch(pattern -> pattern.matcher(className).matches());
+ }
+ }
+}