JPA Interview Questions
💡 Click Show Answer to generate an AI-powered answer instantly.
What is StatelessSession in Hibernate and when would you use it for bulk migration?
Explain composite keys in JPA and how they are implemented.
In JPA, a composite key is a primary key that consists of two or more columns. It's used when a single column isn't sufficient to uniquely identify an entity. JPA provides two main strategies for implementing composite keys: `@EmbeddedId` and `@IdClass`.
What are Composite Keys?
A composite key, also known as a compound key, is a primary key made up of multiple attributes. These attributes, when combined, uniquely identify each record in a table. In the context of relational databases, this is a common practice, and JPA provides mechanisms to map such keys to entities.
Why Use Composite Keys?
- Modeling Many-to-Many Relationships with Attributes: When a join table in a many-to-many relationship needs additional attributes, its primary key often becomes a composite of the foreign keys from the two joined tables.
- Natural Primary Keys: In some domain models, a natural primary key might inherently be composed of multiple fields (e.g.,
(flightNumber, departureDate)for a flight). - Legacy Database Integration: When working with existing databases that already utilize composite keys, JPA must be configured to correctly map these structures.
Implementation Strategies in JPA
JPA offers two primary ways to define composite keys: using @EmbeddedId or @IdClass. Both approaches require the composite key class to be Serializable and correctly implement equals() and hashCode() methods.
@EmbeddedId Strategy
The @EmbeddedId strategy involves creating an embeddable class that represents the composite key. This class is annotated with @Embeddable and contains the fields that form the composite key. The entity then embeds an instance of this class using the @EmbeddedId annotation.
This approach is generally preferred as it encapsulates the primary key logic within a dedicated object, making the entity cleaner and more object-oriented. It's particularly useful when the composite key fields are not part of the entity's regular attributes (e.g., a join table where the PK is just FKs).
import java.io.Serializable;
import java.util.Objects;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.JoinColumn;
// 1. Embeddable Composite Key Class
@Embeddable
public class OrderLineId implements Serializable {
@Column(name = "order_id")
private Long orderId;
@Column(name = "product_id")
private Long productId;
public OrderLineId() {}
public OrderLineId(Long orderId, Long productId) {
this.orderId = orderId;
this.productId = productId;
}
// Getters and Setters
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
// Must implement equals() and hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderLineId that = (OrderLineId) o;
return Objects.equals(orderId, that.orderId) &&
Objects.equals(productId, that.productId);
}
@Override
public int hashCode() {
return Objects.hash(orderId, productId);
}
}
// 2. Entity using the EmbeddedId
@Entity
public class OrderLine {
@EmbeddedId
private OrderLineId id;
@Column(name = "quantity")
private int quantity;
// Can also map the foreign key relationships directly
@ManyToOne
@JoinColumn(name = "order_id", insertable = false, updatable = false)
private Order order;
@ManyToOne
@JoinColumn(name = "product_id", insertable = false, updatable = false)
private Product product;
public OrderLine() {}
// Getters and Setters for id, quantity, order, product
public OrderLineId getId() { return id; }
public void setId(OrderLineId id) { this.id = id; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public Order getOrder() { return order; }
public void setOrder(Order order) { this.order = order; }
public Product getProduct() { return product; }
public void setProduct(Product product) { this.product = product; }
}
@IdClass Strategy
The @IdClass strategy involves defining a separate ID class (not necessarily embeddable) that contains the primary key fields. The entity itself directly defines the primary key fields and marks them with the @Id annotation. Additionally, the entity class is annotated with @IdClass and refers to the separate ID class.
This approach is often simpler to implement for straightforward composite keys where the key fields are directly part of the entity's attributes. However, it can lead to more verbose code in the entity if there are many key fields, as each must be repeated.
import java.io.Serializable;
import java.util.Objects;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
// 1. Separate Composite Key Class
// Does not need @Embeddable
public class BookId implements Serializable {
private String title;
private String authorName;
public BookId() {}
public BookId(String title, String authorName) {
this.title = title;
this.authorName = authorName;
}
// Getters and Setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthorName() { return authorName; }
public void setAuthorName(String authorName) { this.authorName = authorName; }
// Must implement equals() and hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BookId bookId = (BookId) o;
return Objects.equals(title, bookId.title) &&
Objects.equals(authorName, bookId.authorName);
}
@Override
public int hashCode() {
return Objects.hash(title, authorName);
}
}
// 2. Entity using @IdClass
@Entity
@IdClass(BookId.class)
public class Book {
@Id
private String title;
@Id
@Column(name = "author_name")
private String authorName;
@Column(name = "publication_year")
private int publicationYear;
public Book() {}
// Getters and Setters for title, authorName, publicationYear
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthorName() { return authorName; }
public void setAuthorName(String authorName) { this.authorName = authorName; }
public int getPublicationYear() { return publicationYear; }
public void setPublicationYear(int publicationYear) { this.publicationYear = publicationYear; }
}
Choosing the Right Strategy
Both @EmbeddedId and @IdClass achieve the same goal, but each has scenarios where it might be more suitable. The choice often comes down to personal preference, code cleanliness, and specific requirements.
| Feature | @EmbeddedId | @IdClass |
|---|---|---|
| ID Class Structure | Must be annotated with `@Embeddable`. | Can be a plain Java class (no `@Embeddable` required). |
| Entity Fields | Single field of the `@Embeddable` type, annotated with `@EmbeddedId`. | Multiple fields in the entity, each annotated with `@Id` and matching the ID class fields by name and type. |
| Accessing ID Parts | Via the embedded ID object (e.g., `orderLine.getId().getOrderId()`). | Directly on the entity (e.g., `book.getTitle()`). |
| Encapsulation | Better encapsulation of the composite key logic. | Less encapsulation; ID fields are exposed directly on the entity. |
| Readability | Can make entity cleaner, as ID fields are grouped. | Can be slightly more verbose if many ID fields are present on the entity. |
| Use Cases | Often preferred for many-to-many join entities or when ID fields are not 'natural' entity attributes. | Can be simpler for existing entities where ID fields are already part of the entity's attributes. |
Important Considerations
Serializable: Both ID classes must implement theSerializableinterface.equals()andhashCode(): Correct implementation ofequals()andhashCode()methods is critical for composite ID classes. JPA relies on these methods for entity identity and caching.- Default Constructor: ID classes must have a public no-argument constructor.
- Foreign Keys: When a composite key includes foreign keys, ensure that the mapping (e.g.,
@JoinColumn) correctly reflects these relationships, especially with@EmbeddedIdwhereinsertable = falseandupdatable = falseare often needed on the@JoinColumnwhen the ID also defines the relationship.
How do you implement composite keys using Spring Data JPA repositories?
Explain the different inheritance mapping strategies in JPA.
JPA (Java Persistence API) allows mapping Java inheritance hierarchies to database tables. This is crucial for object-oriented design patterns where sub-classes extend super-classes, enabling polymorphic queries and operations. JPA provides three primary strategies to manage how these hierarchies are persisted in the relational database.
1. Single Table Strategy
In the Single Table strategy, the entire inheritance hierarchy (superclass and all subclasses) is mapped to a single database table. A discriminator column is added to this table to distinguish between instances of different concrete classes.
- Annotation:
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) - Discriminator column:
@DiscriminatorColumn(name = "DTYPE", discriminatorType = DiscriminatorType.STRING)
Advantages
- Simplicity: Easy to implement.
- Performance: Excellent for polymorphic queries (e.g., fetching all
Paymentobjects, regardless of type) as it only involves one table. No joins required.
Disadvantages
- Denormalization: The table will contain many nullable columns, as each subclass might have unique fields not relevant to other subclasses or the superclass. This can lead to wasted space and integrity issues.
- Column constraints: Cannot enforce
NOT NULLconstraints on subclass-specific columns directly at the database level for fields that don't apply to all types.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "VEHICLE_TYPE", discriminatorType = DiscriminatorType.STRING)
public abstract class Vehicle {
@Id
private Long id;
private String manufacturer;
// ... getters and setters
}
@Entity
@DiscriminatorValue("CAR")
public class Car extends Vehicle {
private int numberOfDoors;
// ...
}
@Entity
@DiscriminatorValue("BIKE")
public class Bike extends Vehicle {
private boolean hasSidecar;
// ...
}
2. Joined Table Strategy
The Joined Table strategy maps each class in the hierarchy to its own database table. The superclass table stores common attributes, while subclass tables store their specific attributes and also contain a foreign key column referencing the primary key of the superclass table. This forms a one-to-one relationship between a superclass row and its corresponding subclass row.
- Annotation:
@Inheritance(strategy = InheritanceType.JOINED) - Subclass foreign key:
@PrimaryKeyJoinColumn(name="VEHICLE_ID")
Advantages
- Normalization: Data is well-normalized with no nullable columns for subclass-specific fields.
- Data Integrity: Allows enforcing
NOT NULLconstraints and other database-level constraints on all columns. - Flexibility: Better for large hierarchies with many subclass-specific fields.
Disadvantages
- Performance: Polymorphic queries (e.g.,
SELECT * FROM Vehicle) require joins across multiple tables, which can impact performance, especially for deep hierarchies. - Complexity: More complex schema due to multiple tables and join conditions.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Vehicle {
@Id
private Long id;
private String manufacturer;
// ...
}
@Entity
@PrimaryKeyJoinColumn(name = "VEHICLE_ID") // optional, defaults to parent PK name
public class Car extends Vehicle {
private int numberOfDoors;
// ...
}
@Entity
@PrimaryKeyJoinColumn(name = "VEHICLE_ID")
public class Bike extends Vehicle {
private boolean hasSidecar;
// ...
}
3. Table Per Class Strategy
In the Table Per Class strategy, each concrete class in the hierarchy (including abstract superclasses if they are directly queried) is mapped to its own table. There is no table for the abstract superclass. Each table contains columns for all inherited attributes and its own specific attributes. This strategy is generally discouraged by JPA providers due to its limitations.
- Annotation:
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) - Superclass often has
@Entitybut no@Tableif it's abstract and not directly queryable. If concrete, it gets its own table. If it's abstract and mapped, it typically has a table.
Advantages
- Normalization: Each table is self-contained and fully normalized.
- Simplicity: Each concrete class maps to a single table, making it easy to understand for that specific class.
Disadvantages
- No Polymorphic Identity: Hibernate, a popular JPA implementation, does not support polymorphic associations with this strategy by default (e.g., a
Garageentity having aList<Vehicle>will fail without extra configuration). - Performance: Polymorphic queries require
UNION ALLoperations across all subclass tables, which can be very inefficient, especially with many subclasses. - Primary Key Duplication: If
IDENTITYstrategy is used for primary key generation, IDs can clash across tables; typically requiresSEQUENCEorTABLEstrategy. - Abstract Superclass: If the superclass is abstract, no table is created for it. If you query for the abstract superclass, JPA needs to
UNIONall subclass tables.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Vehicle {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE) // IDENTITY won't work across tables
private Long id;
private String manufacturer;
// ...
}
@Entity
public class Car extends Vehicle {
private int numberOfDoors;
// ...
}
@Entity
public class Bike extends Vehicle {
private boolean hasSidecar;
// ...
}
Comparison of Inheritance Strategies
| Feature | Single Table | Joined Table | Table Per Class |
|---|---|---|---|
| Database Schema | 1 table for entire hierarchy | 1 table per class (superclass + subclasses) | 1 table per concrete class |
| Normalization | Poor (many nullable columns) | Excellent | Excellent |
| Polymorphic Queries | Fast (no joins) | Requires joins (can be slow) | Requires UNION ALL (often slowest) |
| Data Integrity | Challenging for subclass-specific fields | Good (via FKs and constraints) | Good (per table) |
| Complexity | Low | Medium | Medium-High (due to PK generation, polymorphic query issues) |
| Recommended Usage | Small, simple hierarchies; few subclass-specific fields. | Complex hierarchies; good normalization needed; frequent queries for specific subclasses. | Generally discouraged due to performance and ORM limitations. Consider only for legacy schemas that strictly require it. |
What are Hibernate event listeners and when are they used?
How do you configure Ehcache as a second-level cache provider in Hibernate or JPA?
Ehcache can be seamlessly integrated with Hibernate or JPA as a second-level cache provider to significantly improve application performance by reducing the number of database calls. The second-level cache stores entity data after it has been loaded from the database, making it available for subsequent queries without hitting the database again. This guide outlines the steps to configure Ehcache.
1. Add Dependencies
First, you need to add the necessary dependencies to your project's build file. This typically includes the Hibernate core, the Hibernate-Ehcache integration library, and potentially a JAXB API dependency for JDK 9+ environments with JPA.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.x.x.Final</version> <!-- Use your Hibernate version -->
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>6.x.x.Final</version> <!-- Must match hibernate-core version -->
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.0</version> <!-- Required for JPA/Hibernate with JDK 9+ -->
</dependency>
2. Configure Persistence Unit (persistence.xml)
In your persistence.xml file, located in the META-INF directory, you need to specify Ehcache as the second-level cache provider and enable caching. You'll also define the caching region factory and, optionally, the default cache concurrency strategy.
<persistence-unit name="myPU">
<properties>
<!-- Enable second-level cache -->
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<!-- Specify Ehcache as the cache provider (for Hibernate 6+) -->
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.jcache.JCacheRegionFactory"/>
<!-- For older Hibernate versions (e.g., Hibernate 5.x) use: -->
<!-- <property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/> -->
<!-- Optional: set default cache mode (e.g., READ_WRITE, NONSTRICT_READ_WRITE, READ_ONLY) -->
<property name="hibernate.cache.default_cache_concurrency_strategy" value="READ_WRITE"/>
<!-- Optional: Enable query cache (stores results of queries, not entities) -->
<!-- <property name="hibernate.cache.use_query_cache" value="true"/> -->
<!-- Optional: Specify ehcache.xml location if not in classpath root -->
<!-- <property name="hibernate.cache.provider_configuration_file_resource_path" value="/config/my-custom-ehcache.xml"/> -->
</properties>
</persistence-unit>
3. Configure Ehcache (ehcache.xml)
Ehcache requires its own configuration file, typically named ehcache.xml, placed in your classpath (e.g., src/main/resources). This file defines cache managers, default cache settings, and specific cache regions for entities or collections. For JCache (JSR-107) compliance, you might use ehcache.xml or ehcache-jcache.xml depending on your version and setup.
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd">
<!-- Default cache settings -->
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="7200"
overflowToDisk="false"
diskPersistent="false"
statistics="true" />
<!-- Define a specific cache for an entity -->
<cache name="com.example.entity.MyEntity"
maxEntriesLocalHeap="5000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="3600"
overflowToDisk="false"
diskPersistent="false"
statistics="true" />
<!-- Define a specific cache for a collection (entity.collectionPropertyName) -->
<cache name="com.example.entity.MyEntity.relatedEntities"
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="900"
timeToLiveSeconds="1800"
overflowToDisk="false"
diskPersistent="false"
statistics="true" />
</ehcache>
4. Enable Caching on Entities
Finally, you need to annotate the entities and their collections that you wish to cache. JPA's @Cacheable annotation marks an entity for caching, and Hibernate's @Cache provides more fine-grained control over cache region and concurrency strategy, allowing you to override the default settings.
import jakarta.persistence.Cacheable;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Column;
import jakarta.persistence.OneToMany;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import java.io.Serializable;
import java.util.List;
@Entity
@Cacheable // JPA annotation to enable caching
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "com.example.entity.MyEntity") // Hibernate specific
public class MyEntity implements Serializable {
@Id
@GeneratedValue
private Long id;
@Column
private String name;
@OneToMany(mappedBy = "myEntity")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // Cache collections
private List<RelatedEntity> relatedEntities;
// Getters, Setters, Constructors...
}
5. Considerations and Best Practices
- Cache Concurrency Strategy: Choose an appropriate strategy (
READ_ONLY,NONSTRICT_READ_WRITE,READ_WRITE,TRANSACTIONAL) based on your data's mutability and consistency requirements.READ_ONLYis fastest but for immutable data,READ_WRITEoffers strong consistency for mutable data. - Cache Regions: Define explicit cache regions in
ehcache.xmlfor better organization and fine-tuning of eviction policies and TTL for different entities or collections. This allows more granular control than relying solely on the default cache. - Eviction Policies: Configure
maxEntriesLocalHeap,timeToIdleSeconds, andtimeToLiveSecondscarefully to manage memory usage and data freshness. In a distributed setup, consideroverflowToDiskor a Terracotta server. - Query Cache: The second-level cache caches entities, while the query cache caches the results of queries. If you frequently run the same queries, enable the query cache (
hibernate.cache.use_query_cache=true) and usequery.setCacheable(true)on your JPA/Hibernate queries. - Serialization: Ensure all cached entities are
Serializableif you are using a distributed cache, or if Ehcache is configured to store objects to disk, as Ehcache might need to serialize them. - Cache Invalidation: Understand how cache invalidation works with your chosen concurrency strategy to prevent stale data issues, especially in clustered environments where multiple application instances might be modifying the database.
- Monitoring: Utilize Ehcache's built-in statistics (enable
statistics="true"inehcache.xml) and JMX integration to monitor cache performance and identify potential bottlenecks or misconfigurations.
How can Redis be used as a second-level cache provider in Hibernate or JPA?
Where is cache stored in JPA when second-level caching is enabled?
When using Ehcache as a second-level cache, where is the cache stored and how is it managed?
What is the difference between JOIN FETCH and Entity Graph in JPA?
Both JOIN FETCH and Entity Graph are powerful features in JPA used to optimize the fetching of associated entities, primarily to prevent the N+1 select problem. While their goal is similar, they differ significantly in their approach, flexibility, and usage.
JOIN FETCH
JOIN FETCH is a clause used within JPQL (JPA Query Language) or Criteria API queries. Its primary purpose is to retrieve related entities along with the root entity in a single SELECT statement. By using JOIN FETCH, the associated collection or single-valued association is eagerly loaded, overriding its default fetch type (e.g., LAZY) for that specific query. This ensures that when the parent entity is accessed, its fetched associations are immediately available without triggering additional database queries.
SELECT p FROM Post p JOIN FETCH p.comments WHERE p.id = :postId
Entity Graph
Entity Graph, introduced in JPA 2.1, provides a more declarative and flexible way to define fetch plans for entities. An Entity Graph can be defined using annotations on the entity class (@NamedEntityGraph) or programmatically. It allows you to specify a 'graph' of attributes (relationships) that should be fetched when an entity is loaded. Entity Graphs can be applied dynamically at runtime to repository methods or EntityManager operations, enabling different fetch strategies for the same entity based on the use case without altering the core queries.
@Entity
@NamedEntityGraph(
name = "post-with-comments",
attributeNodes = @NamedAttributeNode("comments")
)
public class Post {
// ... fields
@OneToMany(mappedBy = "post")
private Set<Comment> comments;
}
// Usage in Spring Data JPA repository:
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(value = "post-with-comments", type = EntityGraph.EntityGraphType.FETCH)
Optional<Post> findById(Long id);
}
Key Differences
While both techniques aim to optimize data fetching, their core characteristics set them apart:
- Declaration & Scope:
JOIN FETCHis part of a JPQL or Criteria API query, making it query-specific. AnEntityGraphis defined separately (either via annotation or API) and can be applied to various queries orEntityManageroperations. - Flexibility:
JOIN FETCHis static for a given query.EntityGraphoffers more flexibility, as different graphs can be applied dynamically at runtime to the same base query orfindByIdoperation. - Fetching Behavior:
JOIN FETCHexplicitly joins and fetches the specified association, effectively overriding its default fetch type to eager for that query.EntityGraphspecifies a 'load plan' which tells the persistence provider which attributes to fetch, often influencing it to use joins or batch fetching. - Inheritance:
JOIN FETCHworks well for single-table inheritance, but can be problematic with joined or table-per-class inheritance strategies due to potential cartesian products or complex queries.EntityGraphhandles inheritance scenarios more gracefully by targeting specific entity types. - Return Type:
JOIN FETCHaffects the result set of the query. If multiple child entities are fetched, they might appear as duplicate parent entities in the result list unlessDISTINCTis used.EntityGraphprimarily influences how the root entity and its specified associations are loaded into the persistence context, typically without affecting the shape of the query result itself (e.g., forfindById).
| Feature | JOIN FETCH | Entity Graph |
|---|---|---|
| Scope | Query-specific (JPQL/Criteria) | Reusable fetch plan (Annotation/API) |
| Declaration | Inline with JPQL/Criteria query | Declared on entity class or programmatically |
| Flexibility | Static for the query | Dynamic, can be chosen at runtime |
| Override Fetch Type | Explicitly overrides (eager for query) | Influences fetching, respects default if not in graph |
| N+1 Prevention | Yes, for fetched associations | Yes, for specified graph attributes |
| Complexity | Simpler for one-off optimizations | More setup for named graphs, flexible API |
When to Use Which
When to use JOIN FETCH
- For specific, one-off queries where you know exactly what associations you need to fetch.
- When the query itself is unique and the fetching strategy doesn't need to be reused.
- In scenarios where you need to filter or order based on the associated entities directly within the same query.
When to use Entity Graph
- When you need reusable fetch plans across different repository methods or
EntityManageroperations. - For dynamic fetching requirements where the required associations might change based on application context (e.g., different UI views needing different data subsets).
- With Spring Data JPA repositories, where
@EntityGraphannotation provides seamless integration. - To define a default fetching strategy for an entity, which can then be overridden or augmented by other graphs.
Conclusion
Both JOIN FETCH and Entity Graph are essential tools for optimizing data access in JPA by mitigating the N+1 problem. JOIN FETCH offers a direct, query-specific approach suitable for ad-hoc optimizations, while Entity Graph provides a more structured, reusable, and dynamic way to define and apply fetch plans, making it ideal for managing complex fetching requirements across an application. The choice between them often depends on the specific use case, required flexibility, and architectural considerations.