The Product Catalog Service is a Supporting Bounded Context within the PakLog fulfillment platform that serves as the authoritative source for all product information, attributes, and relationships. Built with CQRS and optimized for high-read scenarios, it provides consistent product data across all channels while supporting complex hierarchies, variations, and enrichment workflows.
Strategic Importance: HIGH - Foundation for all commerce operations Architecture Pattern: CQRS with Read-Optimized Views Technology Stack: Java 21, Spring Boot 3.2, PostgreSQL, Elasticsearch, Redis Domain Complexity: MEDIUM - Complex data model with simple operations
Core Purpose: Centralized repository for all product master data, providing consistent, enriched product information across all sales channels and operational systems with support for complex product relationships and variations.
Responsibilities (Whatβs IN the Context):
External Dependencies (Whatβs OUT of the Context):
| Term | Definition | Business Context |
|---|---|---|
| SKU | Stock Keeping Unit - unique product variant identifier | Core identifier |
| Product | Sellable item with variants | Master entity |
| Variant | Specific configuration of a product (size, color) | Product instance |
| Attribute | Characteristic of a product (material, brand) | Product metadata |
| Category | Hierarchical classification of products | Organization structure |
| Bundle | Group of products sold together | Product type |
| Kit | Products that must be assembled | Product type |
| Digital Asset | Images, videos, documents for products | Product content |
| GTIN | Global Trade Item Number (barcode) | External identifier |
| Taxonomy | Hierarchical classification system | Data organization |
| Facet | Searchable product attribute | Search feature |
| Enrichment | Process of adding product information | Data quality |
graph TB
subgraph "Product Catalog Bounded Context"
Support1[Supporting: Product Master Data]
Support2[Supporting: Category Management]
Support3[Supporting: Digital Assets]
Support4[Supporting: Product Search]
Generic1[Generic: Data Import/Export]
Generic2[Generic: Event Publishing]
end
Support1 --> Support2
Support1 --> Support3
Support1 --> Support4
Classification: SUPPORTING DOMAIN Strategic Value: HIGH - Enables all commerce operations Investment Priority: MEDIUM - Essential but not differentiating
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@DomainService
public class ProductManagementService {
public Product createProduct(ProductCreationRequest request) {
// Validate unique identifiers
validateUniqueness(request.getSku(), request.getGtin());
// Create product with variants
Product product = Product.builder()
.sku(request.getSku())
.name(request.getName())
.description(request.getDescription())
.category(categoryRepository.find(request.getCategoryId()))
.attributes(enrichAttributes(request.getAttributes()))
.status(ProductStatus.DRAFT)
.build();
// Generate variants if applicable
if (request.hasVariantOptions()) {
List<ProductVariant> variants = variantGenerator
.generate(product, request.getVariantOptions());
product.setVariants(variants);
}
return productRepository.save(product);
}
public Product updateLifecycleState(
SKU sku,
ProductStatus newStatus,
String reason
) {
Product product = productRepository.findBySku(sku);
// Validate state transition
if (!product.canTransitionTo(newStatus)) {
throw new InvalidStateTransitionException(
product.getStatus(),
newStatus
);
}
product.updateStatus(newStatus, reason);
productRepository.save(product);
// Publish state change event
eventPublisher.publish(
new ProductStatusChangedEvent(product, newStatus)
);
return product;
}
}
Classification: SUPPORTING DOMAIN Strategic Value: MEDIUM - Improves navigation and discovery Investment Priority: MEDIUM - Important for user experience
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
@Entity
public class Category {
@Id
private CategoryId id;
private String name;
private String displayName;
private CategoryId parentId;
private Integer level;
private String path; // e.g., "/electronics/computers/laptops"
private List<AttributeDefinition> requiredAttributes;
private List<AttributeDefinition> optionalAttributes;
private CategoryStatus status;
private SortOrder defaultSortOrder;
public boolean isLeaf() {
return !hasChildren();
}
public List<Category> getAncestors() {
// Return path from root to this category
}
public void validateProduct(Product product) {
// Check required attributes are present
requiredAttributes.forEach(attr -> {
if (!product.hasAttribute(attr.getName())) {
throw new MissingRequiredAttributeException(attr);
}
});
}
}
Classification: SUPPORTING DOMAIN Strategic Value: MEDIUM - Critical for online selling Investment Priority: MEDIUM - Direct impact on conversion
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
36
37
@Service
public class DigitalAssetService {
public AssetMetadata uploadProductImage(
SKU sku,
MultipartFile file,
ImageType type
) {
// Validate file
validateImage(file);
// Generate variants
Map<ImageSize, ProcessedImage> variants = imageProcessor
.processImage(file, IMAGE_SIZE_CONFIGS);
// Upload to CDN
Map<ImageSize, String> urls = new HashMap<>();
variants.forEach((size, image) -> {
String url = cdnService.upload(
generatePath(sku, type, size),
image
);
urls.put(size, url);
});
// Save metadata
AssetMetadata metadata = AssetMetadata.builder()
.sku(sku)
.type(type)
.originalFilename(file.getOriginalFilename())
.urls(urls)
.uploadedAt(LocalDateTime.now())
.build();
return assetRepository.save(metadata);
}
}
Classification: SUPPORTING DOMAIN Strategic Value: HIGH - Directly impacts conversion Investment Priority: HIGH - Critical for user experience
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Service
public class ProductSearchService {
private final ElasticsearchClient elasticsearchClient;
public SearchResults search(SearchRequest request) {
// Build Elasticsearch query
BoolQuery.Builder queryBuilder = new BoolQuery.Builder();
// Add text search
if (request.hasSearchTerm()) {
queryBuilder.must(m -> m
.multiMatch(mm -> mm
.query(request.getSearchTerm())
.fields(List.of(
"name^3", // Boost name matches
"brand^2", // Boost brand matches
"description",
"attributes"
))
.type(MultiMatchType.BestFields)
.fuzziness(Fuzziness.AUTO)
)
);
}
// Add filters
request.getFilters().forEach(filter -> {
queryBuilder.filter(f -> f
.term(t -> t
.field(filter.getField())
.value(filter.getValue())
)
);
});
// Add facet aggregations
Map<String, Aggregation> aggregations = buildFacetAggregations(
request.getFacets()
);
// Execute search
SearchResponse<ProductDocument> response = elasticsearchClient
.search(s -> s
.index("products")
.query(queryBuilder.build()._toQuery())
.aggregations(aggregations)
.from(request.getOffset())
.size(request.getLimit())
.sort(buildSortOptions(request.getSort())),
ProductDocument.class
);
return mapToSearchResults(response);
}
}
Classification: GENERIC DOMAIN Strategic Value: LOW - Utility function Investment Priority: LOW - Use standard tools
classDiagram
class Product {
<<Aggregate Root>>
-ProductId id
-SKU masterSku
-String name
-String description
-Brand brand
-Category category
-List~ProductVariant~ variants
-List~Attribute~ attributes
-ProductStatus status
-ProductDimensions dimensions
-LocalDateTime createdAt
+createVariant()
+updateAttribute()
+changeStatus()
+addDigitalAsset()
+assignCategory()
}
class ProductVariant {
<<Entity>>
-SKU sku
-String variantName
-Map~String_String~ options
-List~Attribute~ variantAttributes
-VariantStatus status
-GTIN gtin
-Weight weight
-Dimensions dimensions
+activate()
+deactivate()
}
class Bundle {
<<Entity>>
-BundleId id
-SKU bundleSku
-String name
-List~BundleComponent~ components
-BundleType type
+addComponent()
+removeComponent()
+validateComponents()
}
class DigitalAsset {
<<Entity>>
-AssetId id
-SKU sku
-AssetType type
-String filename
-Map~Size_URL~ urls
-AssetMetadata metadata
-Integer displayOrder
+updateOrder()
+replaceFile()
}
Product --> ProductVariant
Product --> DigitalAsset
Bundle --> Product
classDiagram
class SKU {
<<Value Object>>
-String value
-SKUType type
+validate()
+format()
}
class GTIN {
<<Value Object>>
-String value
-GTINType type
+validate()
+checkDigit()
}
class ProductDimensions {
<<Value Object>>
-Measurement length
-Measurement width
-Measurement height
-Weight weight
+volume()
+dimensionalWeight()
}
class Attribute {
<<Value Object>>
-String name
-String value
-AttributeType type
-String unit
+validate()
+format()
}
class Category {
<<Value Object>>
-CategoryId id
-String name
-String path
-Integer level
+isChild()
+getParent()
}
| Event | Trigger | Consumers | Purpose |
|---|---|---|---|
ProductCreatedEvent |
New product added | Search, Inventory | Index and initialize |
ProductUpdatedEvent |
Product modified | Search, Cache | Update indexes |
ProductActivatedEvent |
Product made available | Inventory, Pricing | Enable selling |
ProductDeactivatedEvent |
Product discontinued | Order, Inventory | Stop selling |
VariantCreatedEvent |
New variant added | Inventory | Create stock position |
CategoryChangedEvent |
Product recategorized | Search | Update navigation |
AssetUploadedEvent |
New image/document | CDN, Search | Process and index |
BundleCreatedEvent |
Bundle configured | Pricing, Inventory | Setup components |
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
L1: Product Information Management
βββ L2: Product Master Data
β βββ L3: Product Creation & Management
β βββ L3: Variant Management
β βββ L3: Attribute Management
β βββ L3: Bundle Configuration
β βββ L3: Lifecycle Management
βββ L2: Product Organization
β βββ L3: Category Management
β βββ L3: Taxonomy Management
β βββ L3: Brand Management
β βββ L3: Collection Management
β βββ L3: Relationship Management
βββ L2: Content Management
β βββ L3: Description Management
β βββ L3: Digital Asset Management
β βββ L3: Multi-language Content
β βββ L3: SEO Optimization
β βββ L3: Content Versioning
βββ L2: Product Discovery
β βββ L3: Search & Filtering
β βββ L3: Faceted Navigation
β βββ L3: Recommendations
β βββ L3: Similar Products
β βββ L3: Product Comparison
βββ L2: Data Quality
βββ L3: Data Validation
βββ L3: Enrichment Workflows
βββ L3: Completeness Scoring
βββ L3: Duplicate Detection
βββ L3: Data Governance
Business Goal: Provide accurate, complete product information across all channels
Key Business Outcomes:
Purpose: Maintain authoritative product information
Business Rules:
Creation Workflow:
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
36
37
38
39
40
41
42
43
@Service
@Transactional
public class ProductCreationWorkflow {
public Product createProduct(ProductRequest request) {
// Step 1: Validate request
ValidationResult validation = validator.validate(request);
if (!validation.isValid()) {
throw new ValidationException(validation.getErrors());
}
// Step 2: Check for duplicates
checkForDuplicates(request);
// Step 3: Enrich with defaults
request = enrichWithDefaults(request);
// Step 4: Create product
Product product = Product.create(
request.getSku(),
request.getName(),
request.getDescription()
);
// Step 5: Set attributes
request.getAttributes().forEach(attr -> {
product.setAttribute(attr.getName(), attr.getValue());
});
// Step 6: Assign category
Category category = categoryRepository.find(request.getCategoryId());
product.assignToCategory(category);
// Step 7: Validate against category rules
category.validateProduct(product);
// Step 8: Save and publish
Product saved = productRepository.save(product);
eventPublisher.publish(new ProductCreatedEvent(saved));
return saved;
}
}
Purpose: Handle product variations (size, color, etc.)
Variant Generation:
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
@Component
public class VariantGenerator {
public List<ProductVariant> generate(
Product product,
List<VariantOption> options
) {
// Generate all combinations
List<Map<String, String>> combinations =
generateCombinations(options);
return combinations.stream()
.map(combo -> ProductVariant.builder()
.masterProduct(product)
.sku(generateVariantSku(product.getSku(), combo))
.options(combo)
.name(generateVariantName(product.getName(), combo))
.status(VariantStatus.DRAFT)
.build()
)
.collect(Collectors.toList());
}
private String generateVariantSku(SKU masterSku, Map<String, String> options) {
// Example: SHIRT-001 -> SHIRT-001-RED-L
StringBuilder sku = new StringBuilder(masterSku.getValue());
options.values().forEach(value -> {
sku.append("-").append(value.toUpperCase());
});
return sku.toString();
}
}
Purpose: Organize products in navigable hierarchy
Category Structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Root
βββ Electronics
β βββ Computers
β β βββ Laptops
β β βββ Desktops
β β βββ Tablets
β βββ Audio
β βββ Headphones
β βββ Speakers
βββ Clothing
βββ Men's
β βββ Shirts
β βββ Pants
βββ Women's
βββ Dresses
βββ Shoes
Navigation Generation:
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
@Service
public class NavigationService {
public NavigationTree buildNavigation() {
List<Category> rootCategories = categoryRepository.findRoots();
return NavigationTree.builder()
.categories(rootCategories.stream()
.map(this::buildCategoryNode)
.collect(Collectors.toList())
)
.build();
}
private CategoryNode buildCategoryNode(Category category) {
return CategoryNode.builder()
.id(category.getId())
.name(category.getDisplayName())
.url(category.getUrl())
.productCount(getProductCount(category))
.children(category.getChildren().stream()
.filter(Category::isActive)
.map(this::buildCategoryNode)
.collect(Collectors.toList())
)
.build();
}
}
Purpose: Manage product images, videos, and documents
Asset Types:
Image Processing Pipeline:
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
@Component
public class ImageProcessingPipeline {
private static final Map<ImageSize, ImageConfig> SIZE_CONFIGS = Map.of(
ImageSize.THUMBNAIL, new ImageConfig(150, 150, 85),
ImageSize.SMALL, new ImageConfig(300, 300, 90),
ImageSize.MEDIUM, new ImageConfig(600, 600, 90),
ImageSize.LARGE, new ImageConfig(1200, 1200, 95),
ImageSize.ORIGINAL, new ImageConfig(null, null, 100)
);
public Map<ImageSize, ProcessedImage> process(MultipartFile file) {
BufferedImage original = ImageIO.read(file.getInputStream());
return SIZE_CONFIGS.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> processForSize(original, entry.getValue())
));
}
private ProcessedImage processForSize(
BufferedImage original,
ImageConfig config
) {
if (config.isOriginal()) {
return new ProcessedImage(original);
}
BufferedImage resized = resize(original, config);
BufferedImage optimized = optimize(resized, config);
return new ProcessedImage(optimized);
}
}
Purpose: Enable product finding and discovery
Search Features:
Faceted Search:
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
36
37
@Service
public class FacetedSearchService {
public FacetedSearchResults search(FacetedSearchRequest request) {
// Build base query
Query query = buildQuery(request);
// Add facet aggregations
SearchSourceBuilder searchSource = new SearchSourceBuilder()
.query(query)
.size(request.getPageSize())
.from(request.getOffset());
// Add dynamic facets
request.getRequestedFacets().forEach(facet -> {
searchSource.aggregation(
AggregationBuilders
.terms(facet.getName())
.field(facet.getField())
.size(facet.getMaxBuckets())
);
});
// Execute search
SearchResponse response = elasticsearchClient.search(
new SearchRequest("products").source(searchSource),
RequestOptions.DEFAULT
);
// Process results
return FacetedSearchResults.builder()
.products(extractProducts(response))
.facets(extractFacets(response))
.totalCount(response.getHits().getTotalHits().value)
.build();
}
}
Purpose: Measure and improve product data quality
Scoring Algorithm:
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
36
37
38
39
40
41
42
43
44
45
@Component
public class CompletenessScorer {
public CompletenessScore calculate(Product product) {
int totalFields = 0;
int completedFields = 0;
// Check required fields
Map<String, Integer> requiredFields = Map.of(
"name", 10,
"description", 10,
"category", 10,
"primaryImage", 15,
"price", 10,
"dimensions", 5,
"weight", 5
);
requiredFields.forEach((field, weight) -> {
totalFields += weight;
if (hasField(product, field)) {
completedFields += weight;
}
});
// Check category-specific attributes
Category category = product.getCategory();
category.getRequiredAttributes().forEach(attr -> {
totalFields += 5;
if (product.hasAttribute(attr.getName())) {
completedFields += 5;
}
});
// Calculate score
double score = (completedFields * 100.0) / totalFields;
return CompletenessScore.builder()
.score(score)
.level(determineLevel(score))
.missingFields(findMissingFields(product))
.suggestions(generateSuggestions(product))
.build();
}
}
graph TB
PC[Product Catalog] -->|Product Data| INV[Inventory]
PC -->|Product Data| PRICE[Pricing]
PC -->|Product Data| SEARCH[Search]
PC -->|Product Data| OMS[Order Management]
VENDOR[Vendor System] -->|Product Updates| PC
PIM[External PIM] -->|Bulk Import| PC
style PC fill:#bbf,stroke:#333,stroke-width:4px
style INV fill:#f9f,stroke:#333,stroke-width:2px
style PRICE fill:#f9f,stroke:#333,stroke-width:2px
style SEARCH fill:#f9f,stroke:#333,stroke-width:2px
style OMS fill:#f9f,stroke:#333,stroke-width:2px
style VENDOR fill:#ffa,stroke:#333,stroke-width:2px
style PIM fill:#ffa,stroke:#333,stroke-width:2px
Pattern: OPEN HOST SERVICE
API Contract:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/api/v1/products:
get:
summary: Search products
parameters:
- name: q
description: Search query
- name: category
description: Category filter
- name: attributes
description: Attribute filters
responses:
200:
schema:
$ref: '#/components/schemas/ProductList'
/api/v1/products/{sku}:
get:
summary: Get product details
responses:
200:
schema:
$ref: '#/components/schemas/Product'
Pattern: ANTI-CORRUPTION LAYER
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
36
37
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Product Catalog Service β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Command Side β β
β β β β
β β ββββββββββββββββ ββββββββββββββββ β β
β β β Product β β Category β β β
β β β Management β β Management β β β
β β ββββββββ¬ββββββββ ββββββββ¬ββββββββ β β
β β β β β β
β β ββββββββΌβββββββββββββββββββββΌββββββ β β
β β β PostgreSQL (Write Model) β β β
β β ββββββββββββββββ¬ββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βββββββββββΌβββββββββββ β
β β Event Publisher β β
β βββββββββββ¬βββββββββββ β
β β β
β βββββββββββββββββββΌβββββββββββββββββββββββββββββββ β
β β Query Side β β
β β β β
β β ββββββββββββββββ ββββββββββββββββ β β
β β β Elasticsearchβ β Redis β β β
β β β (Search) β β (Cache) β β β
β β ββββββββ²ββββββββ ββββββββ²ββββββββ β β
β β β β β β
β β ββββββββ΄βββββββββββββββββββββ΄ββββββ β β
β β β Event Processors β β β
β β ββββββββββββββββββββββββββββββββββββ β β
β β β β
β β ββββββββββββββββ ββββββββββββββββ β β
β β β Search API β β Read API β β β
β β ββββββββββββββββ ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Language | Java | 21 | Core programming language |
| Framework | Spring Boot | 3.2.0 | Application framework |
| Write Store | PostgreSQL | 15.0 | Master data storage |
| Search Engine | Elasticsearch | 8.11 | Full-text search |
| Cache | Redis | 7.2 | High-speed lookups |
| CDN | CloudFront | Latest | Asset delivery |
| Messaging | Apache Kafka | 3.5 | Event streaming |
| Storage | S3 | Latest | Digital assets |
| API Docs | SpringDoc OpenAPI | 2.3.0 | API documentation |
| Metric | Target | Actual | Status |
|---|---|---|---|
| Product Query | <100ms | 45ms | β |
| Search Response | <500ms | 320ms | β |
| Asset Upload | <5 sec | 3.2 sec | β |
| Bulk Import | 1000/min | 1500/min | β |
| Cache Hit Rate | >90% | 94% | β |
| Search Relevance | >95% | 96% | β |
| KPI | Description | Target | Current | Impact |
|---|---|---|---|---|
| Data Completeness | % products with all required fields | 95% | 97% | Better conversions |
| Search Relevance | % searches with click on first page | 95% | 96% | User satisfaction |
| Content Quality | Average quality score | 90% | 92% | SEO improvement |
| Update Latency | Time to propagate changes | <1 min | 45 sec | Operational efficiency |
| Asset Load Time | Image delivery speed | <200ms | 150ms | Page performance |
| Risk | Probability | Impact | Mitigation Strategy |
|---|---|---|---|
| Data Inconsistency | Medium | High | CQRS with eventual consistency |
| Search Index Corruption | Low | High | Regular backups, rebuild capability |
| Asset Storage Failure | Low | Medium | Multi-region S3, CDN caching |
| Performance Degradation | Medium | Medium | Caching, read replicas |
| Risk | Probability | Impact | Mitigation Strategy |
|---|---|---|---|
| Incorrect Product Info | Medium | High | Validation rules, approval workflow |
| Missing Images | Low | Medium | Fallback images, monitoring |
| Category Misclassification | Medium | Medium | ML-based suggestions, review |
Document Version: 1.0.0 Last Updated: 2025-01-20 Status: APPROVED Next Review: 2025-04-20