Design Decision: Final Vendor ↔ Item (Many-to-Many)Relationship in GoProcure (Association + Composition)


Introduction

As we continue building the GoProcure enterprise procurement system, one of the most crucial design decisions we had to revisit was the relationship between Vendor and Item.

At first glance, it might seem simple — “a Vendor supplies many Items.” But in real-world enterprise procurement, this relationship is more nuanced.

In this article, I’ll walk you through the reasoning behind our final design, why we changed it, and how we implemented it in both .NET (EF Core) and Java (JPA/Hibernate).


Understanding the Procurement Context

In GoProcure, the Item Catalog represents all products or materials that may be needed across various departments.

  • These items belong to the organization, not to any specific Vendor.

  • Vendors can supply multiple Items.

  • Each Item can be supplied by multiple Vendors, possibly at different prices, lead times, or quality ratings.

  • The Vendor selection happens when a Purchase Request (PR) or Purchase Order (PO) is created, not when the Item is defined.

This understanding shifted our perspective on how to model these relationships.


Our Initial Design (and Why We Changed It)

Initially, we modeled Vendor → Item as a One-to-Many Aggregation. The idea was that a Vendor “owned” its Items.

However, this quickly proved incorrect when we considered scenarios like:

  • Multiple Vendors supplying the same product (e.g., “Office Chair”).

  • Reassigning vendors without recreating items.

  • Retaining historical purchase data even if a vendor is deactivated.

The initial model caused unnecessary coupling between Vendors and Items. So, we revised it into a cleaner, more flexible structure.


Final Design Summary

Relationship Type Cardinality Ownership Delete Behavior
Vendor ↔ Item Association Many-to-Many None Independent
Vendor ↔ VendorItem Composition One-to-Many Vendor owns VendorItem Cascade Delete
Item ↔ VendorItem Association One-to-Many None Restrict

Explanation:

  • Vendor and Item are independent aggregates.

  • VendorItem is a linking entity (association class) that captures the relationship details such as price, delivery period, availability, and rating.

  • When a Vendor is deleted, all its VendorItems go with it (composition).

  • However, the Items remain in the catalog (association).


UML Representation

vendor-item manay to many relationship

  • ◆ = Composition (Vendor owns VendorItem)

  • o = Association (Item is linked via VendorItem)

This clearly illustrates that Vendor and Item are independent, connected through the associative entity VendorItem.


Implementation in .NET (EF Core)

modelBuilder.Entity<VendorItem>()

.HasKey(vi => new { vi.VendorId, vi.ItemId });

modelBuilder.Entity<Vendor>()
    .HasMany(v => v.VendorItems)
    .WithOne(vi => vi.Vendor)
    .HasForeignKey(vi => vi.VendorId)
    .OnDelete(DeleteBehavior.Cascade); // Composition-like relationship
modelBuilder.Entity<Item>()
    .HasMany(i => i.VendorItems)
    .WithOne(vi => vi.Item)
    .HasForeignKey(vi => vi.ItemId)
    .OnDelete(DeleteBehavior.Restrict); // Association

Explanation:

  • The composite key (VendorId, ItemId) ensures each Vendor-Item pair is unique.

  • Cascading deletes from Vendor ensure dependent VendorItems are removed automatically.

  • Restrict delete on Item prevents losing Vendor history when removing an Item.


Implementation in Java (JPA / Hibernate)

@Entity
public class VendorItem {
    @EmbeddedId
    private VendorItemId id;
   @ManyToOne
  @MapsId("vendorId")
   @JoinColumn(name = "vendor_id")
   private Vendor vendor;
   @ManyToOne
   @MapsId("itemId")
   @JoinColumn(name = "item_id")
   private Item item;
   private BigDecimalprice;
   private int deliveryDays;
   private boolean active;
}
@Embeddable
public class VendorItemId implements Serializable {
   private UUID vendorId;
   private UUID itemId;
}

Explanation:

  • VendorItem uses a composite key (VendorItemId) to link Vendor and Item.

  • @MapsId maps the embedded key fields to the respective foreign keys.

  • Cascading rules are applied via @OneToMany(mappedBy = ..., cascade = CascadeType.ALL) in Vendor.


Why This Design Matters

Data Integrity — prevents unintended deletions while keeping relationships consistent.
Flexibility — supports multiple Vendors per Item and vice versa.
Scalability — easily extended to include attributes like rating, minOrderQty, or contractExpiry.
Realism — accurately models enterprise procurement processes.


Key Takeaways

  • Relationships must reflect business semantics, not just data structure.

  • A Vendor supplying Items is association, not ownership.

  • The lifecycle of linked entities (like VendorItem) defines whether it’s composition or aggregation.

  • Both EF Core and JPA allow us to implement these relationships cleanly with proper delete behavior and composite keys.


Next Steps

In the next part of this series, we’ll begin implementing the Domain Models as Classes in .NET and Java, while adhering to best practices in Domain-Driven Design and Clean Architecture.

Stay tuned 🚀
Follow the #CodeTrip journey to learn how we turn great design into production-ready code.


Leave a Comment

Your email address will not be published. Required fields are marked *