Designing a Global Supply Chain ERD with PlantUML

In the complex world of global logistics, the ability to track a single container across oceans, borders, and multiple carriers is the backbone of modern supply chain visibility. As e-commerce and international trade accelerate, logistics platforms must handle massive volumes of data involving shippers, freight forwarders, vessels, and ports, all while maintaining real-time accuracy. A robust database schema is not just a technical requirement; it is the architectural foundation that enables stakeholders to predict ETAs, detect delays, and manage exceptions before they escalate into costly disruptions.

Real-world system context and operational workflow illustration

Visualizing this intricate ecosystem through an Entity-Relationship Diagram (ERD) is critical for architects and developers before writing a single line of SQL. By using PlantUML, a diagram-as-code approach, teams can define these relationships declaratively, ensuring consistency and versioning without the drag-and-drop limitations of traditional tools. This tutorial leverages VPasCode to build a comprehensive ERD for a global shipment visibility platform, demonstrating how to model the lifecycle of a container from booking to delivery, including the nuanced relationships between voyages, tracking events, and exception alerts.

Understanding the Model: Purpose, Scope & Problem Framing

The diagram we are about to construct represents the core data backbone of a Global Supply Chain Shipment Visibility and Container Tracking Platform. This is not merely a list of tables; it is a relational map of the physical and digital flow of goods across international trade lanes.

Diagram Abstraction & Representation

In this ERD, we model the entities as the fundamental nouns of the logistics domain: the Shipper, the Vessel, the Port, and the Container. The relationships (lines) represent the verbs: books, transports, carries, and generates. The cardinality (one-to-many, zero-to-many) is crucial here. For instance, a single Shipment can contain many Containers, but a Container belongs to one specific Shipment at a given time. This abstraction allows us to capture the complexity of multi-container shipments and the hierarchical nature of voyages.

Target Domain Scope & Scenario

The scope of this model covers the end-to-end journey of cargo. It explicitly models the booking phase (Shipper, Consignee, Forwarder), the transport phase (Vessel, Voyage, Port), and the monitoring phase (Container Events, Exception Alerts). It intentionally excludes lower-level warehouse management details to focus on the transit visibility that is critical for global tracking. The diagram defines how a Shipment is linked to a Carrier, how a Container is linked to a Voyage, and how Tracking Events provide the timestamped history required for real-time dashboards.

Key Takeaways & Educational Insights

By the end of this walkthrough, you will understand how to:

  • Define complex entities with primary keys (UUIDs) and foreign keys (UUIDs) using PlantUML syntax.
  • Model one-to-many relationships critical for logistics hierarchies (e.g., one vessel, many voyages).
  • Structure the ContainerEvent entity to support granular tracking at ports and locations.
  • Implement an alerting system (ExceptionAlert) that ties back to specific shipments and events.

Complete Diagram & Full Source Code

Before we dive into the step-by-step construction, here is the complete, rendered blueprint. This diagram illustrates the full relational structure of the logistics platform, utilizing the sunlust theme for a professional, warm aesthetic.

Global Supply Chain Shipment Visibility and Container Tracking Platform - ERD

@startuml
/' 
Diagram Context:
This ERD models a global supply chain shipment visibility and container tracking platform.
It captures the core entities involved in moving goods across international trade lanes:
shippers, carriers, freight forwarders, shipments, containers, vessels, ports, tracking
events, and exception alerts. The model supports end-to-end visibility by linking
shipments to their containers, containers to vessels and voyages, and every milestone
or deviation to a timestamped tracking event. It is intended to help logistics teams,
customers, and operations staff monitor cargo status, predict ETAs, and respond to
delays, customs holds, or route deviations in near real time.
'/
!theme sunlust

title Global Supply Chain Shipment Visibility and Container Tracking Platform - ERD

entity "Shipper" as shipper {
  * shipper_id : UUID <<PK>>
  --
  company_name : VARCHAR
  contact_name : VARCHAR
  email : VARCHAR
  phone : VARCHAR
  country : VARCHAR
  created_at : TIMESTAMP
}

entity "Consignee" as consignee {
  * consignee_id : UUID <<PK>>
  --
  company_name : VARCHAR
  contact_name : VARCHAR
  email : VARCHAR
  phone : VARCHAR
  country : VARCHAR
  created_at : TIMESTAMP
}

entity "FreightForwarder" as forwarder {
  * forwarder_id : UUID <<PK>>
  --
  company_name : VARCHAR
  scac_code : VARCHAR
  contact_email : VARCHAR
  country : VARCHAR
}

entity "Carrier" as carrier {
  * carrier_id : UUID <<PK>>
  --
  carrier_name : VARCHAR
  scac_code : VARCHAR
  carrier_type : VARCHAR
  country : VARCHAR
}

entity "Vessel" as vessel {
  * vessel_id : UUID <<PK>>
  --
  imo_number : VARCHAR
  mmsi_number : VARCHAR
  vessel_name : VARCHAR
  vessel_type : VARCHAR
  flag_country : VARCHAR
}

entity "Port" as port {
  * port_id : UUID <<PK>>
  --
  unlocode : VARCHAR
  port_name : VARCHAR
  country : VARCHAR
  latitude : DECIMAL
  longitude : DECIMAL
}

entity "Shipment" as shipment {
  * shipment_id : UUID <<PK>>
  --
  booking_number : VARCHAR
  bill_of_lading : VARCHAR
  shipper_id : UUID <<FK>>
  consignee_id : UUID <<FK>>
  forwarder_id : UUID <<FK>>
  carrier_id : UUID <<FK>>
  origin_port_id : UUID <<FK>>
  destination_port_id : UUID <<FK>>
  shipment_status : VARCHAR
  planned_departure : TIMESTAMP
  planned_arrival : TIMESTAMP
  actual_departure : TIMESTAMP
  actual_arrival : TIMESTAMP
  created_at : TIMESTAMP
}

entity "Container" as container {
  * container_id : UUID <<PK>>
  --
  container_number : VARCHAR
  container_type : VARCHAR
  seal_number : VARCHAR
  tare_weight_kg : DECIMAL
  max_payload_kg : DECIMAL
  shipment_id : UUID <<FK>>
  current_status : VARCHAR
  current_location : VARCHAR
}

entity "Voyage" as voyage {
  * voyage_id : UUID <<PK>>
  --
  voyage_number : VARCHAR
  vessel_id : UUID <<FK>>
  carrier_id : UUID <<FK>>
  departure_port_id : UUID <<FK>>
  arrival_port_id : UUID <<FK>>
  scheduled_departure : TIMESTAMP
  scheduled_arrival : TIMESTAMP
  actual_departure : TIMESTAMP
  actual_arrival : TIMESTAMP
}

entity "ContainerEvent" as container_event {
  * event_id : UUID <<PK>>
  --
  container_id : UUID <<FK>>
  voyage_id : UUID <<FK>>
  event_type : VARCHAR
  event_timestamp : TIMESTAMP
  location_name : VARCHAR
  port_id : UUID <<FK>>
  latitude : DECIMAL
  longitude : DECIMAL
  source_system : VARCHAR
  remarks : TEXT
}

entity "ExceptionAlert" as exception_alert {
  * alert_id : UUID <<PK>>
  --
  shipment_id : UUID <<FK>>
  container_id : UUID <<FK>>
  event_id : UUID <<FK>>
  alert_type : VARCHAR
  severity : VARCHAR
  description : TEXT
  detected_at : TIMESTAMP
  resolved_at : TIMESTAMP
  status : VARCHAR
}

entity "User" as user {
  * user_id : UUID <<PK>>
  --
  full_name : VARCHAR
  email : VARCHAR
  role : VARCHAR
  organization : VARCHAR
  created_at : TIMESTAMP
}

entity "ShipmentSubscription" as subscription {
  * subscription_id : UUID <<PK>>
  --
  user_id : UUID <<FK>>
  shipment_id : UUID <<FK>>
  notify_on : VARCHAR
  channel : VARCHAR
  created_at : TIMESTAMP
}

shipper ||--o{ shipment : "books"
consignee ||--o{ shipment : "receives"
forwarder ||--o{ shipment : "manages"
carrier ||--o{ shipment : "transports"
port ||--o{ shipment : "origin"
port ||--o{ shipment : "destination"

shipment ||--o{ container : "contains"
carrier ||--o{ voyage : "operates"
vessel ||--o{ voyage : "sails"
port ||--o{ voyage : "departs"
port ||--o{ voyage : "arrives"

container ||--o{ container_event : "generates"
voyage ||--o{ container_event : "context"
port ||--o{ container_event : "occurs_at"

shipment ||--o{ exception_alert : "raises"
container ||--o{ exception_alert : "triggers"
container_event ||--o{ exception_alert : "based_on"

user ||--o{ subscription : "follows"
shipment ||--o{ subscription : "tracked_by"

@enduml

Step-by-Step Architectural Walkthrough

Building a complex ERD requires a structured approach. We will construct this diagram in four logical phases: setting the theme, defining the core entities, establishing the relationships, and finally, adding the event tracking and subscription layers.

Phase 1: Canvas Configuration & Layout Directives

Every PlantUML diagram begins with configuration directives that set the stage for the rendering engine. In this logistics model, we use the !theme sunlust directive. This applies a pre-defined color palette (warm oranges, deep blues, and clean whites) that is highly readable and professional, suitable for enterprise architecture documentation.

Additionally, we define a comment block using the /' and '/ syntax. This is not rendered as a diagram element but serves as a crucial piece of documentation embedded directly in the code, explaining the scope and purpose of the ERD for future maintainers.

/' 
Diagram Context:
This ERD models a global supply chain shipment visibility...
'/
!theme sunlust

title Global Supply Chain Shipment Visibility and Container Tracking Platform - ERD

Phase 2: Declaring Core Entities, Actors, and Boundaries

The first step in modeling the data is to define the entity blocks. In PlantUML ERD, an entity is defined with a name, an alias, and a list of attributes. We use the * symbol to denote Primary Keys (PK) and UUID as the data type, which is a modern best practice for distributed systems to avoid key collisions.

For example, the Shipper and Consignee entities capture the commercial parties. The Shipment entity is the central hub, linking the commercial parties (Shipper, Consignee, Forwarder, Carrier) to the physical logistics nodes (Ports).

entity "Shipper" as shipper {
  * shipper_id : UUID <>
  --
  company_name : VARCHAR
  contact_name : VARCHAR
  email : VARCHAR
  phone : VARCHAR
  country : VARCHAR
  created_at : TIMESTAMP
}

Notice the -- separator. This visually divides the identifier (PK) from the descriptive attributes, improving readability when the diagram renders.

Phase 3: Mapping Data Flows & Key Interactions

Once entities are defined, we must establish the relationships. PlantUML uses specific syntax for cardinality. The ||--o{ syntax reads as: “One (or many) on the left, Zero or One on the right.” This is perfect for logistics. For instance, a Shipper (left) can book zero or many Shipments (right).

We model the physical movement of goods through the Voyage entity. A Voyage connects a Vessel, a Carrier, and two Ports (departure and arrival). This structure allows the system to track which ship carried which cargo on a specific trip.

carrier ||--o{ voyage : "operates"
vessel ||--o{ voyage : "sails"
port ||--o{ voyage : "departs"
port ||--o{ voyage : "arrives"

The Shipment entity links to Origin and Destination ports, creating the route definition. The Shipment also links to Containers, indicating that a single booking might contain multiple physical units.

Phase 4: Grouping, Annotations & Visual Polish

The final phase adds the dynamic layers: Tracking Events and Alerts. In a real-time system, tracking events are the heartbeat. The ContainerEvent entity is linked to the Container, the Voyage, and the Port where the event occurred. This triad allows the system to answer questions like: “Where was container X when the delay was detected?”

We also introduce the ExceptionAlert entity. This is a critical component for logistics operations. It links back to the Shipment, Container, and the triggering ContainerEvent. This creates a traceable chain of custody from the alert back to the physical event.

Finally, we add the User and ShipmentSubscription entities. This models the notification system, allowing users to subscribe to specific shipments and receive alerts via email or SMS.

user ||--o{ subscription : "follows"
shipment ||--o{ subscription : "tracked_by"

Syntax & Keyword Deep Dive

To master PlantUML ERD, you must understand the specific keywords and symbols used in this diagram. Here is a breakdown of the critical syntax elements:

  • entity: The primary directive to define a database table. It takes the form entity "Name" as alias { ... }.
  • * (Asterisk):** Used immediately before an attribute name (e.g., * shipper_id) to indicate a Primary Key. This is rendered with a bold or distinct visual style in the diagram.
  • UUID & FK: While PlantUML is a diagramming tool, using standard SQL types like UUID and the stereotype <> (Foreign Key) helps developers visualize the database schema directly from the diagram.
  • ||--o{: This is the relationship syntax. || means “One or Many” (the mandatory side), -- is the line, and o{ means “Zero or One” (the optional side). This accurately reflects the cardinality of a Shipper booking Shipment.
  • "label": The text inside quotes (e.g., "books") defines the relationship name, making the diagram self-documenting.
  • !theme: A directive to apply a predefined color theme to the entire diagram, ensuring consistency without manual styling.

Best Practices & Pitfalls to Avoid

When designing ERDs for complex logistics systems, adherence to best practices ensures the diagram remains a useful tool rather than a source of confusion.

  1. Use Meaningful Aliases: As seen in the code, we used as shipper instead of as Shipper. PlantUML is case-sensitive for aliases. Using lowercase aliases for the relationship lines (e.g., shipper ||--o{ shipment) keeps the code clean and prevents syntax errors.
  2. Separate Identifiers from Attributes: Always use the -- separator to visually and logically separate the Primary Key from the rest of the attributes. This mimics standard SQL schema definitions and improves readability.
  3. Consistent Cardinality: Ensure your relationship arrows match the real-world logic. For example, a Port can be the origin for many shipments, but a shipment has only one origin. Using ||--o{ correctly captures this “One-to-Many” directionality.
  4. Avoid Over-Modeling: While this diagram is comprehensive, remember to only model entities that are critical to the specific problem (visibility and tracking). Avoid adding internal HR or finance tables unless they are directly relevant to the shipment lifecycle.

Start Building Logistics ERDs Faster with VPasCode

Instantly visualize your supply chain data models, test relationship cardinalities, and export professional diagrams in seconds with our free web-based PlantUML editor.

Scroll to Top