The Product Catalog Service is the canonical source of truth for all product master data across the fulfillment ecosystem. It manages product information including SKUs, descriptions, dimensions, weights, and special handling requirements such as hazardous materials.
Architecture Pattern: Hexagonal Architecture with Domain-Driven Design (DDD) Technology Stack: Spring Boot 3, Spring Data MongoDB, Spring Kafka, CQRS Integration Pattern: Event-Driven Architecture with Published Language
The Product Catalog bounded context is responsible for maintaining the master data for all products that flow through the fulfillment network.
Responsibilities (Whatβs IN):
External Dependencies (Whatβs OUT):
Core Domain Terms:
Strategic Importance: MEDIUM - Supporting domain but critical for operations
This is a supporting domain that enables fulfillment operations through:
Subdomains:
Description: The central aggregate representing a product with all its attributes and characteristics.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@AggregateRoot
public class Product {
private String sku; // Aggregate ID
private String name;
private String description;
private Dimensions dimensions;
private Weight weight;
private ProductAttributes attributes;
private boolean archived;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private int version; // Optimistic locking
// Business methods
public void updateDimensions(Dimensions newDimensions);
public void updateWeight(Weight newWeight);
public void updateAttributes(ProductAttributes newAttributes);
public void archive();
public void restore();
public boolean isHazmat();
public boolean isFragile();
public boolean isOversized(Dimensions threshold);
// Invariants
private void ensureSkuIsValid();
private void ensureItemDimensionsNotExceedPackage();
private void ensureHazmatDataComplete();
private void ensureWeightIsPositive();
}
Invariants:
Domain Events:
ProductCreatedEventProductUpdatedEventProductArchivedEventProductRestoredEventProductDeletedEvent1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@ValueObject
public class Dimensions {
private ItemDimension item;
private PackageDimension package_;
public Volume getItemVolume();
public Volume getPackageVolume();
public double getPackageEfficiency(); // item volume / package volume
public boolean itemFitsInPackage();
public DimensionalWeight calculateDimensionalWeight(int dimFactor);
}
@ValueObject
public class ItemDimension {
private BigDecimal length;
private BigDecimal width;
private BigDecimal height;
private DimensionUnit unit; // INCHES, CENTIMETERS
public Volume calculateVolume();
public boolean fitsWithin(PackageDimension package_);
}
@ValueObject
public class PackageDimension {
private BigDecimal length;
private BigDecimal width;
private BigDecimal height;
private DimensionUnit unit;
public Volume calculateVolume();
public boolean canAccommodate(ItemDimension item);
}
1
2
3
4
5
6
7
8
9
@ValueObject
public class Weight {
private BigDecimal value;
private WeightUnit unit; // POUNDS, KILOGRAMS, OUNCES
public Weight convertTo(WeightUnit targetUnit);
public boolean isHeavy(Weight threshold);
public boolean exceedsLimit(Weight maxWeight);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ValueObject
public class ProductAttributes {
private boolean fragile;
private boolean perishable;
private boolean highValue;
private boolean oversized;
private boolean batteryPowered;
private boolean liquid;
private HazmatInfo hazmat; // null if not hazmat
public boolean requiresSpecialHandling();
public Set<HandlingRequirement> getHandlingRequirements();
public boolean hasShippingRestrictions();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ValueObject
public class HazmatInfo {
private String unNumber; // UN1266
private String hazardClass; // 3 (Flammable liquids)
private String packingGroup; // II
private String properShippingName;
private boolean airEligible;
private boolean groundOnly;
private List<String> specialInstructions;
public boolean isComplete();
public boolean canShipVia(ShippingMethod method);
public String formatForShippingLabel();
}
Responsibility: Validate product data for consistency and business rules.
1
2
3
4
5
6
7
8
9
10
@DomainService
public class ProductValidationService {
public ValidationResult validate(Product product);
private ValidationResult validateSKU(String sku);
private ValidationResult validateDimensions(Dimensions dimensions);
private ValidationResult validateWeight(Weight weight);
private ValidationResult validateHazmatInfo(HazmatInfo hazmat);
}
Responsibility: Calculate dimensional weight for shipping cost estimation.
1
2
3
4
5
6
7
8
9
10
11
12
13
@DomainService
public class DimensionalWeightCalculator {
public DimensionalWeight calculate(
PackageDimension dimensions,
int dimFactor // carrier-specific: FedEx=139, UPS=139
);
public Weight getChargeableWeight(
Weight actualWeight,
DimensionalWeight dimWeight
); // Returns greater of actual or dimensional
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Commands
public interface CreateProductCommand {
Product execute(CreateProductRequest request);
}
public interface UpdateProductCommand {
Product execute(UpdateProductRequest request);
}
public interface ArchiveProductCommand {
void execute(String sku);
}
public interface RestoreProductCommand {
void execute(String sku);
}
// Queries (Separate query services for CQRS)
public interface GetProductQuery {
Product execute(String sku);
}
public interface ListProductsQuery {
PagedResult<Product> execute(int offset, int limit, boolean includeArchived);
}
public interface SearchProductsQuery {
List<Product> execute(String searchTerm);
}
public interface GetProductDimensionsQuery {
Dimensions execute(String sku);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Repository ports
public interface ProductRepository {
Optional<Product> findBySku(String sku);
List<Product> findAllActive(Pageable pageable);
List<Product> findBySkus(List<String> skus);
void save(Product product);
void delete(String sku);
boolean existsBySku(String sku);
}
// Event publishing
public interface EventPublisher {
void publish(DomainEvent event);
}
REST Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<ProductResponse> createProduct(
@Valid @RequestBody CreateProductRequest request
);
@GetMapping("/{sku}")
public ResponseEntity<ProductResponse> getProduct(
@PathVariable String sku
);
@GetMapping
public ResponseEntity<PagedProductResponse> listProducts(
@RequestParam(defaultValue = "0") int offset,
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "false") boolean includeArchived
);
@PutMapping("/{sku}")
public ResponseEntity<ProductResponse> updateProduct(
@PathVariable String sku,
@Valid @RequestBody UpdateProductRequest request
);
@DeleteMapping("/{sku}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public ResponseEntity<Void> archiveProduct(@PathVariable String sku);
@PostMapping("/{sku}/restore")
public ResponseEntity<ProductResponse> restoreProduct(@PathVariable String sku);
}
MongoDB Adapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Repository
public class MongoProductRepository implements ProductRepository {
private final MongoTemplate mongoTemplate;
@Override
public Optional<Product> findBySku(String sku) {
return Optional.ofNullable(
mongoTemplate.findById(sku, Product.class)
);
}
@Override
public List<Product> findAllActive(Pageable pageable) {
Query query = new Query(Criteria.where("archived").is(false))
.with(pageable);
return mongoTemplate.find(query, Product.class);
}
@Override
public void save(Product product) {
mongoTemplate.save(product);
}
}
Kafka Event Publisher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class KafkaProductEventPublisher implements EventPublisher {
private static final String TOPIC = "product.catalog.v1.events";
private final KafkaTemplate<String, CloudEvent> kafkaTemplate;
@Override
public void publish(DomainEvent event) {
CloudEvent cloudEvent = CloudEventBuilder.v1()
.withId(event.getId())
.withType(event.getType())
.withSource(URI.create("product-catalog-service"))
.withData(event.toJson().getBytes())
.build();
kafkaTemplate.send(TOPIC, cloudEvent);
}
}
Canonical source of truth for all product information used across inventory, cartonization, warehouse operations, and shipment services.
ProductCreatedEventProductUpdatedEventProductArchivedEvent1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"specversion": "1.0",
"type": "com.paklog.product.created",
"source": "product-catalog-service",
"id": "evt-prod-12345",
"time": "2025-10-18T10:30:00Z",
"datacontenttype": "application/json",
"data": {
"sku": "PROD-12345",
"name": "Widget Pro",
"description": "Premium widget with advanced features",
"dimensions": {
"item": {
"length": 10.0,
"width": 5.0,
"height": 3.0,
"unit": "INCHES"
},
"package": {
"length": 12.0,
"width": 7.0,
"height": 5.0,
"unit": "INCHES"
}
},
"weight": {
"value": 2.5,
"unit": "POUNDS"
},
"attributes": {
"fragile": false,
"hazmat": false
},
"createdAt": "2025-10-18T10:30:00Z"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"specversion": "1.0",
"type": "com.paklog.product.updated",
"source": "product-catalog-service",
"id": "evt-prod-update-456",
"time": "2025-10-18T11:00:00Z",
"datacontenttype": "application/json",
"data": {
"sku": "PROD-12345",
"changes": {
"dimensions": {
"before": {"item": {"length": 10.0, "width": 5.0, "height": 3.0}},
"after": {"item": {"length": 11.0, "width": 5.0, "height": 3.0}}
}
},
"updatedAt": "2025-10-18T11:00:00Z"
}
}
The Product Catalog Service is a well-defined bounded context:
Business Impact: Supports millions of SKUs, >95% data quality, <50ms query latency, >90% cache hit rate, real-time event propagation.