Skip to content

Schema Intent: Embedding vs References

Overview

This document clarifies the architectural intent behind the ABS Platform GraphQL schema design, specifically addressing when to use embedding vs references for object relationships. The schema syntax alone (field: Type!) is insufficient to convey implementation intent - this document provides the modeler's intent for proper implementation.

Core Principles

1. GraphQL Syntax vs Implementation Intent

GraphQL Syntax: field: Type! only defines the interface (returns Type object) Implementation Intent: Determined by the modeler's requirements, not the syntax

2. Field Name Compatibility Pattern

CRITICAL: JSON data field names must match GraphQL schema field names exactly for direct compatibility.

  • Field Names: Must match schema field names (e.g., services, service_bundle, payment_history)
  • Data Types: Dictated by schema type definitions (e.g., [Service!]!, ServiceBundle!, [PaymentAction]!)
  • Resolver Logic: Handles ID-to-object resolution automatically based on schema type

Example:

# Schema defines:
type ServiceBundle {
  services: [Service!]!  # Field name: "services", Type: array of Service objects
}

// JSON data must use:
{
  "services": ["svc-1", "svc-2"]  // Field name matches schema, values are IDs
}

The resolver automatically resolves the ID array to Service objects based on the schema type definition.

3. Size-Based Decision Making

For larger objects, it is beneficial to use references to: - Avoid data duplication - Enable independent analysis - Support sharing across multiple entities - Optimize storage and performance

4. Relationship Types

Relationship Type Intent Implementation Reasoning
Reference, shared, N:1 Multiple entities can reference the same object Store separately with ID, resolve via reference Object is reusable across multiple entities
Reference, 1:1, separately stored for convenience One-to-one relationship, but stored separately Store separately with ID, resolve via reference Object can be analyzed independently
Embedded, 1:1 Object is owned by and specific to the parent Store inline with parent object Object is not independently useful

Schema Intent Clarifications

IMPORTANT: This explanation applies to ALL objects in the schema. Every relationship between objects follows these principles.

Universal Application

Every object relationship in the ABS Platform schema follows the same decision-making process:

  1. Is the object large enough to benefit from separate storage?
  2. Is the object shared across multiple entities?
  3. Is the object useful for independent analysis?

Examples from the Schema

ServiceAccount → ServiceBundle Relationship

# ServiceAccount type
type ServiceAccount {
  # INTENT: Reference, shared, N:1 (multiple ServiceAccounts can use same bundle)
  # IMPLEMENTATION: Resolver resolves from service_bundle field (contains ID)
  # REASONING: ServiceBundle is a product/offer that multiple customers can subscribe to
  # SIZE: Large object - beneficial to use reference for sharing
  service_bundle: ServiceBundle!
}

Intent: Reference, shared, N:1 - Why: ServiceBundle is a product/offer that multiple customers can subscribe to - Implementation: Store ServiceBundle separately, reference by ID in service_bundle field - JSON Data: "service_bundle": "bundle-swap-freedom" (field name matches schema) - Benefit: One ServiceBundle can be used by multiple ServiceAccounts

ServicePlan → ServiceAccount Relationship

# ServicePlan type
type ServicePlan {
  # INTENT: Reference, 1:1, separately stored for convenience
  # IMPLEMENTATION: Resolver resolves from service_account field (contains ID)
  # REASONING: ServiceAccount can be analyzed independently, but is plan-specific
  # SIZE: Large object - beneficial to use reference for analytical convenience
  service_account: ServiceAccount!
}

Intent: Reference, 1:1, separately stored for convenience - Why: ServiceAccount can be analyzed independently, but is plan-specific - Implementation: Store ServiceAccount separately, reference by ID in service_account field - JSON Data: "service_account": "svc-acc-001" (field name matches schema) - Benefit: ServiceAccount can be analyzed independently for business intelligence

ServicePlan → PaymentAccount Relationship

# ServicePlan type
type ServicePlan {
  # INTENT: Reference, 1:1, separately stored for convenience
  # IMPLEMENTATION: Resolver resolves from payment_account field (contains ID)
  # REASONING: PaymentAccount can be analyzed independently, but is plan-specific
  # SIZE: Large object - beneficial to use reference for analytical convenience
  payment_account: PaymentAccount!
}

Intent: Reference, 1:1, separately stored for convenience - Why: PaymentAccount can be analyzed independently, but is plan-specific - Implementation: Store PaymentAccount separately, reference by ID in payment_account field - JSON Data: "payment_account": "pay-acc-001" (field name matches schema) - Benefit: PaymentAccount can be analyzed independently for financial reporting

All Other Schema Objects

The same principles apply to ALL other objects in the schema:

  • ServicePlanTemplateCommonTerms: Reference, shared, N:1
  • ServicePlanTemplateMealyFSM: Reference, shared, N:1
  • ServicePlanTemplateAgentConfig: Reference, shared, N:1
  • ServiceBundleService: Reference, shared, N:1
  • ServiceAccountServiceState: Embedded, 1:1 (small objects)
  • ServiceAccountServiceAction: Reference, 1:1, separately stored for analytical convenience
  • PaymentAccountPaymentAction: Reference, 1:1, separately stored for analytical convenience
  • MealyFSMMealyTransition: Embedded, 1:1 (small objects)
  • ServicePlanTemplateTemplateChange: Embedded, 1:1 (small objects)
  • LocationPhysicalAddress: Reference, shared, N:1

Every object relationship in the schema follows these same decision-making criteria.

Additional Schema Examples

ServiceBundle → Service Relationship

# ServiceBundle type
type ServiceBundle {
  # INTENT: Reference, shared, N:1 (multiple ServiceBundles can reference same Service)
  # IMPLEMENTATION: Resolver resolves from services field (contains array of IDs)
  # REASONING: Services are standalone objects referencable by any other entity
  # SIZE: Large objects - beneficial to use reference for sharing
  services: [Service!]!
}

Intent: Reference, shared, N:1 - Why: Services are standalone objects referencable by any other entity - Implementation: Store Service separately, reference by ID array in services field - JSON Data: "services": ["svc-1", "svc-2"] (field name matches schema) - Benefit: One Service can be used by multiple ServiceBundles

ServiceAccount → ServiceState Relationship

# ServiceState type
# INTENT: Embedded, 1:1, specific to ServiceAccount
# IMPLEMENTATION: Store inline with ServiceAccount object
# REASONING: ServiceState is specific to ServiceAccount and cannot exist independently
# SIZE: Small object - beneficial to embed for simplicity
type ServiceState {
  service_id: ID!
  current_asset: String
  used: Float!
  quota: Float!
}

Intent: Embedded, 1:1 - Why: ServiceState is specific to ServiceAccount and cannot exist independently - Implementation: Store ServiceState inline with ServiceAccount - Benefit: Simpler data structure, no separate storage needed

MealyFSM → MealyTransition Relationship

# MealyFSM type
type MealyFSM {
  # INTENT: Embedded, 1:1, integral to FSM
  # IMPLEMENTATION: Store inline with MealyFSM object
  # REASONING: Transition array is integral to the FSM and has no meaning outside of FSM
  # SIZE: Small objects - beneficial to embed for simplicity
  transitions: [MealyTransition!]!
}

Intent: Embedded, 1:1 - Why: Transition array is integral to the FSM and has no meaning outside of FSM - Implementation: Store MealyTransition inline with MealyFSM - Benefit: FSM and its transitions are always together, no separate storage needed

ServiceAccount → ServiceAction Relationship

# ServiceAccount type
type ServiceAccount {
  # INTENT: Reference, 1:1, separately stored for analytical convenience
  # IMPLEMENTATION: Resolver resolves from service_history field (contains array of IDs)
  # REASONING: ServiceAction can be analyzed independently for statistics across service plans
  # SIZE: Small objects but beneficial to use reference for analytical purposes
  service_history: [ServiceAction]!
}

# ServiceAction type
# INTENT: Reference, 1:1, separately stored for analytical convenience
# IMPLEMENTATION: Store separately with ID, resolve via reference
# REASONING: ServiceAction can be analyzed independently for statistics across service plans
# SIZE: Small object but beneficial to use reference for analytical purposes
type ServiceAction {
  id: ID!
  service_type: String!
  service_amount: Float!
}

Intent: Reference, 1:1, separately stored for analytical convenience - Why: ServiceAction can be analyzed independently for statistics across service plans - Implementation: Store ServiceAction separately, reference by ID array in service_history field - JSON Data: "service_history": ["action-1", "action-2"] (field name matches schema) - Benefit: ServiceAction can be analyzed independently for business intelligence

PaymentAccount → PaymentAction Relationship

# PaymentAccount type
type PaymentAccount {
  # INTENT: Reference, 1:1, separately stored for analytical convenience
  # IMPLEMENTATION: Resolver resolves from payment_history field (contains array of IDs)
  # REASONING: PaymentAction can be analyzed independently for statistics across service plans
  # SIZE: Small objects but beneficial to use reference for analytical purposes
  payment_history: [PaymentAction]!
}

# PaymentAction type
# INTENT: Reference, 1:1, separately stored for analytical convenience
# IMPLEMENTATION: Store separately with ID, resolve via reference
# REASONING: PaymentAction can be analyzed independently for statistics across service plans
# SIZE: Small object but beneficial to use reference for analytical purposes
type PaymentAction {
  id: ID!
  payment_type: String!
  payment_amount: Float!
}

Intent: Reference, 1:1, separately stored for analytical convenience - Why: PaymentAction can be analyzed independently for statistics across service plans - Implementation: Store PaymentAction separately, reference by ID array in payment_history field - JSON Data: "payment_history": ["pay-action-1", "pay-action-2"] (field name matches schema) - Benefit: PaymentAction can be analyzed independently for financial reporting

Location → PhysicalAddress Relationship

# Location type
type Location {
  # INTENT: Reference, shared, N:1 (multiple Locations can reference same PhysicalAddress)
  # IMPLEMENTATION: Resolver resolves from physical_address field (contains ID)
  # REASONING: A physical location can be used by ANY entity
  # SIZE: Medium object - beneficial to use reference for sharing
  physical_address: PhysicalAddress
}

Intent: Reference, shared, N:1 - Why: A physical location can be used by ANY entity - Implementation: Store PhysicalAddress separately, reference by ID in physical_address field - JSON Data: "physical_address": "addr-001" (field name matches schema) - Benefit: One PhysicalAddress can be used by multiple Locations

Implementation Guidelines

1. Field Name Compatibility

CRITICAL: JSON data field names must exactly match GraphQL schema field names.

  • Field Names: Use schema field names (e.g., services, service_bundle, payment_history)
  • Data Values: Store IDs or ID arrays as appropriate for the schema type
  • Resolver Logic: Automatically resolves IDs to objects based on schema type definitions

2. ID Field Usage

  • Include ID field: When object should be stored separately
  • Omit ID field: When object is truly embedded and not independently useful

3. Referential Integrity

For 1:1 relationships that use references: - Enforce uniqueness at the implementation level - Maintain referential integrity through database constraints - Handle cascading operations appropriately

4. Resolver Implementation

Resolvers should: - Use schema field names for direct compatibility - Resolve from ID values when object is referenced - Return embedded object when object is truly embedded - Handle ID-to-object resolution automatically based on schema type

Examples

Reference Implementation (ServiceBundle)

// ServiceBundle stored separately
const serviceBundle = {
  id: "bundle-swap-freedom",
  name: "Swap Freedom Package",
  // ... other fields
};

// ServiceAccount references ServiceBundle by ID (field name matches schema)
const serviceAccount = {
  id: "svc-acc-001",
  service_bundle: "bundle-swap-freedom", // Field name matches schema
  // ... other fields
};

// Resolver resolves ServiceBundle from ID
const resolveServiceBundle = (serviceAccount) => {
  return getServiceBundleById(serviceAccount.service_bundle);
};

Reference Implementation (ServiceAccount)

// ServiceAccount stored separately
const serviceAccount = {
  id: "svc-acc-001",
  service_bundle: "bundle-swap-freedom", // Field name matches schema
  // ... other fields
};

// ServicePlan references ServiceAccount by ID (field name matches schema)
const servicePlan = {
  id: "plan-001",
  service_account: "svc-acc-001", // Field name matches schema
  // ... other fields
};

// Resolver resolves ServiceAccount from ID
const resolveServiceAccount = (servicePlan) => {
  return getServiceAccountById(servicePlan.service_account);
};

Benefits of This Approach

1. Data Sharing

  • ServiceBundle can be used by multiple ServiceAccounts
  • Reduces data duplication
  • Enables consistent updates across all references

2. Analytical Convenience

  • ServiceAccount and PaymentAccount can be analyzed independently
  • Supports business intelligence and reporting
  • Enables cross-plan analysis

3. Performance Optimization

  • Large objects are stored separately
  • Reduces memory usage for parent objects
  • Enables efficient querying and indexing

4. Maintainability

  • Clear separation of concerns
  • Easier to update shared objects
  • Better data organization

Conclusion

The ABS Platform schema uses a reference-based architecture for larger objects to enable sharing, analytical convenience, and performance optimization. The schema comments clearly convey the modeler's intent, ensuring proper implementation by developers.

Key Takeaways: 1. Field Name Compatibility: JSON data field names must exactly match GraphQL schema field names for direct compatibility 2. Data Type Dictation: The actual data type (ID vs object) is dictated by the schema type definition 3. Resolver Automation: Resolvers automatically handle ID-to-object resolution based on schema types 4. Intent Clarification: Always clarify the modeler's intent when implementing GraphQL schemas, as the syntax alone is insufficient to convey the implementation approach