Entity Bean Creation Guide for AI Agents
August 6, 2026 · View on GitHub
Target Audience: AI systems (Claude, Copilot, ChatGPT, etc.) Purpose: Learn how to generate clean, idiomatic Ebean entity beans Key Insight: Ebean entity fields must be non-public (no public fields). Accessors don't need JavaBeans naming conventions; no manual equals/hashCode implementation is needed Language: Java Framework: Ebean ORM
Quick Rules
Before writing entity code, remember:
| Requirement | Needed? | Notes |
|---|---|---|
@Entity annotation | ✅ YES | Marks class as persistent entity |
@Id annotation | ✅ YES | Marks primary key field |
| Getters/setters (or other accessors) | ✅ YES | Needed for application code to access fields. Naming can be JavaBeans, fluent, or custom — no specific convention required. |
| Default constructor | ❌ NO | Not required. Ebean can instantiate without it. |
| equals/hashCode | ❌ NO | Ebean auto-enhances these at compile time. |
| toString() | ❌ NO | Ebean auto-enhances this. Don't implement with getters. |
@Version | ⚠️ OPTIONAL | Use for optimistic locking. Highly recommended. |
@WhenCreated | ⚠️ OPTIONAL | Auto-timestamp creation time. Highly recommended. Use for audit trail. |
@WhenModified | ⚠️ OPTIONAL | Auto-timestamp modification time. Highly recommended. Use for audit trail. |
Critical:
- Prefer primitive
longfor@Idand@Version, NOTLongobject. - Fields should be non-public: private, protected, or package-private.
- If you add accessors, they do NOT need to follow Java bean conventions.
Naming Conventions: The D* (Domain) Prefix Pattern
Entity beans represent internal domain/persistence model details. It's a common best practice in Ebean projects to use the D prefix* (D for Domain) for entity class names.
Why use D prefix?*
- Avoid name clashes with DTOs - Your public API may have
Customer(DTO), but your entity isDCustomer(Domain). They're clearly different. - Signal intent clearly - The D prefix immediately tells developers "this is an internal domain class, not part of the public API"
- Clarify conversions - When converting
DCustomer→Customer(DTO), the direction is obvious - Separate concerns - API classes in one package (no prefix), domain classes in another (with D prefix)
Example naming pattern:
- Entity:
DCustomer,DOrder,DProduct,DInvoice - DTO:
Customer,Order,Product,Invoice - Converter:
DCustomerMapper.toDTO(DCustomer)→Customer
Where to place entities:
- Entities:
com.example.domain.entity(orpersistence) - DTOs:
com.example.api.modelorcom.example.dto
When to use D prefix:*
- ✅ DO use for entity beans (internal domain model)
- ✅ DO use when you have parallel DTO classes with similar names
- ❌ DON'T use for DTOs or public API classes
- ❌ DON'T use if you have no DTOs and entities are your public API
Example with and without prefix:
// With D* prefix (recommended - allows both entity and DTO to exist)
@Entity
public class DCustomer {
@Id private long id;
private String name;
// ... entity-specific fields and methods
}
// Public API DTO (no D prefix)
public record Customer(long id, String name) {
// ... conversion method
}
// Conversion
public static Customer toDTO(DCustomer entity) {
return new Customer(entity.getId(), entity.getName());
}
This naming convention is optional but highly recommended for projects with separate domain and API layers.
Minimal Entity (No Boilerplate)
This is a complete, valid Ebean entity:
@Entity
public class Customer {
@Id
private long id;
private String name;
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Why this works:
- ✅
@Entitymarks it as persistent - ✅
@Id private long idis the primary key - ✅ Private fields (Ebean does NOT support public fields without expert flags enabled)
- ✅ Accessors can follow any naming convention, and can be omitted when field access is preferred
- ✅ No default constructor needed
- ✅ No equals/hashCode needed (Ebean enhances these)
What Ebean does at compile time:
- Enhances equals/hashCode based on @Id
- Adds field change tracking
- Enables lazy loading
- Enhances toString()
Result: Your entity is now fully functional with zero boilerplate.
Pattern 1: Basic Entity
Use this when: You need a simple persistent object.
@Entity
public class Product {
@Id
private long id;
private String name;
private String description;
private BigDecimal price;
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
}
What you get:
- Primary key:
id(private field, accessed via getter) - Three properties:
name,description,price(private fields, accessed via getters/setters) - Automatic equals/hashCode based on id
- Full ORM functionality
Pattern 2: Entity with Audit Trail
Use this when: You need to track who/when created/modified data.
@Entity
public class Order {
@Id
private long id;
@Version
private long version;
@WhenCreated
private Instant createdAt;
@WhenModified
private Instant modifiedAt;
private String orderNumber;
private BigDecimal totalAmount;
public long getId() {
return id;
}
public long getVersion() {
return version;
}
public Instant getCreatedAt() {
return createdAt;
}
public Instant getModifiedAt() {
return modifiedAt;
}
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
}
What you get:
version: Optimistic locking (prevents concurrent update conflicts)createdAt: Automatically set when inserted (Ebean manages this)modifiedAt: Automatically updated on every modification (Ebean manages this)
Example usage:
// Create
Order order = new Order();
order.setOrderNumber("ORD-001");
order.setTotalAmount(new BigDecimal("99.99"));
database.save(order); // createdAt is automatically set by Ebean
// Modify
order.setTotalAmount(new BigDecimal("109.99"));
database.update(order); // version incremented, modifiedAt updated automatically
// Check when modified
System.out.println(order.getModifiedAt()); // Current timestamp
Pattern 3: Entity with Constructor
Use this when: Domain logic requires initialization or validation.
@Entity
public class Invoice {
@Id
private long id;
@Version
private long version;
private String invoiceNumber;
private String customerName;
private BigDecimal amount;
public Invoice(String invoiceNumber, String customerName, BigDecimal amount) {
this.invoiceNumber = invoiceNumber;
this.customerName = customerName;
this.amount = amount;
}
public long getId() {
return id;
}
public long getVersion() {
return version;
}
public String getInvoiceNumber() {
return invoiceNumber;
}
public String getCustomerName() {
return customerName;
}
public BigDecimal getAmount() {
return amount;
}
}
When to add a constructor:
- ✅ Required fields must be set during creation
- ✅ Validation needs to happen on initialization
- ✅ Domain logic needs setup
When NOT to add:
- ❌ If users will just set fields afterwards anyway
- ❌ If there are many optional fields
Pattern 4: Entity with Relationships
Use this when: You need associations to other entities.
@Entity
public class Customer {
@Id
private long id;
@Version
private long version;
@WhenCreated
private Instant createdAt;
private String name;
private String email;
@OneToMany(mappedBy = "customer")
private List<Order> orders; // Use List, not Set
@ManyToOne
private Address billingAddress;
public long getId() {
return id;
}
public long getVersion() {
return version;
}
public Instant getCreatedAt() {
return createdAt;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public List<Order> getOrders() {
return orders;
}
public Address getBillingAddress() {
return billingAddress;
}
public void setBillingAddress(Address billingAddress) {
this.billingAddress = billingAddress;
}
}
Important:
- Use
List<>notSet<>for collections (Set calls equals/hashCode before beans have IDs) mappedBymeans Order.customer is the owner- Relationships are lazy-loaded by default — except
@OneToOne(mappedBy=...), which defaults toFetchType.EAGERand adds aleft jointo every default select of the owning entity. Explicitly setfetch = FetchType.LAZYon@OneToOne(mappedBy=...)fields unless the association is needed on (almost) every load. See "@OneToOne(mappedBy=...)is EAGER by default" in the query-beans guide for details.
What NOT to Do (Anti-Patterns)
❌ Anti-Pattern 1: Public Fields
DON'T:
@Entity
public class Customer {
@Id public long id; // ❌ Public field - not supported
public String name; // ❌ Public field - not supported
}
DO:
@Entity
public class Customer {
@Id
private long id; // ✅ Private field with getter
private String name; // ✅ Private field with accessors
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Why: Ebean does NOT support public fields. Fields must be private and accessed via getters/setters or other accessor methods. Public fields bypass Ebean's tracking mechanisms and will cause data consistency issues.
❌ Anti-Pattern 2: Use Long Object Instead of Primitive
DON'T:
@Entity
public class Customer {
@Id
private Long id; // ❌ Object type
private String name;
}
DO:
@Entity
public class Customer {
@Id
private long id; // ✅ Primitive type
private String name;
}
Why: Performance, nullability semantics, Ebean optimization.
❌ Anti-Pattern 3: Implement equals/hashCode
DON'T:
@Entity
public class Customer {
@Id
private long id;
private String name;
@Override
public boolean equals(Object o) { // ❌ Unnecessary
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return id == customer.id;
}
@Override
public int hashCode() { // ❌ Unnecessary
return Objects.hash(id);
}
}
DO:
@Entity
public class Customer {
@Id
private long id;
private String name;
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// Ebean enhances equals/hashCode automatically
}
Why: Ebean's enhancement is optimized for ORM operations. Your implementation might conflict with Ebean's tracking.
❌ Anti-Pattern 4: Use Set for Collections
DON'T:
@Entity
public class Customer {
@Id
private long id;
@OneToMany(mappedBy = "customer")
private Set<Order> orders; // ❌ Set calls equals/hashCode before IDs assigned
}
DO:
@Entity
public class Customer {
@Id
private long id;
@OneToMany(mappedBy = "customer")
private List<Order> orders; // ✅ List doesn't require equals/hashCode on unsaved beans
}
Why: Set calls equals/hashCode immediately. New beans don't have IDs yet, causing issues.
❌ Anti-Pattern 5: toString() with Getters
DON'T:
@Entity
public class Customer {
@Id
private long id;
private String name;
@Override
public String toString() { // ❌ Uses getters
return "Customer{" +
"id=" + getId() +
", name='" + getName() + '\'' +
'}';
}
public long getId() { return id; }
public String getName() { return name; }
}
Why: In a debugger, toString() is called automatically. Getters can trigger lazy loading, changing debug behavior.
DO: Either don't implement toString(), or access fields directly:
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
❌ Anti-Pattern 6: @Column(name=...) for Naming Convention
DON'T:
@Entity
public class Customer {
@Id
private long id;
@Column(name = "first_name") // ❌ Unnecessary
private String firstName;
}
DO:
@Entity
public class Customer {
@Id
private long id;
private String firstName; // ✅ Ebean uses naming convention: first_name
}
Why: Ebean's naming convention handles this automatically. Only use @Column when your database column doesn't match the convention.
What Ebean Enhancement Provides
At compile time, Ebean enhances your entity classes:
- equals/hashCode - Based on @Id, optimal for ORM
- Field change tracking - Knows which fields were modified
- Lazy loading - Collections and relationships load on demand
- Persistence context - Manages identity and state
- toString() - Auto-implemented (don't override with getters)
Result: Your entity bean is minimal, but fully featured.
Field Types
Recommended for ID/Version:
long(primitive) ✅ Use thisint(primitive) ✅ Use thisUUID✅ Use this
Not recommended:
Longobject ⚠️ Avoid (use primitive long)Integerobject ⚠️ Avoid (use primitive int)
For other fields:
- Use standard Java types:
String,BigDecimal,Instant,LocalDate, etc. - Use primitives where nullable semantics don't apply:
int,long,boolean - Use objects where null has meaning:
String,BigDecimal,LocalDate
Example: Building an Entity Step by Step
Start minimal, add what you need:
Step 1: Minimal
@Entity
public class BlogPost {
@Id long id;
String title;
String content;
public long getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
Step 2: Add audit trail
@Entity
public class BlogPost {
@Id long id;
@Version long version;
@WhenCreated Instant createdAt;
@WhenModified Instant modifiedAt;
String title;
String content;
public long getId() { return id; }
public long getVersion() { return version; }
public Instant getCreatedAt() { return createdAt; }
public Instant getModifiedAt() { return modifiedAt; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
Step 3: Add author relationship
@Entity
public class BlogPost {
@Id long id;
@Version long version;
@WhenCreated Instant createdAt;
@WhenModified Instant modifiedAt;
String title;
String content;
@ManyToOne
Author author;
public long getId() { return id; }
public long getVersion() { return version; }
public Instant getCreatedAt() { return createdAt; }
public Instant getModifiedAt() { return modifiedAt; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Author getAuthor() { return author; }
public void setAuthor(Author author) { this.author = author; }
}
Step 4: Add constructor if needed
@Entity
public class BlogPost {
@Id long id;
@Version long version;
@WhenCreated Instant createdAt;
@WhenModified Instant modifiedAt;
String title;
String content;
@ManyToOne
Author author;
public BlogPost(String title, String content, Author author) {
this.title = title;
this.content = content;
this.author = author;
}
public long getId() { return id; }
public long getVersion() { return version; }
public Instant getCreatedAt() { return createdAt; }
public Instant getModifiedAt() { return modifiedAt; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Author getAuthor() { return author; }
public void setAuthor(Author author) { this.author = author; }
}
Each step adds only what's necessary for the entity structure. Accessors are always included so application code can use the entity.
Usage Examples
Creating and saving
Customer customer = new Customer();
customer.setName("Alice");
database.save(customer); // id auto-generated
Finding
Customer found = database.find(Customer.class, 1);
System.out.println(found.getName());
Updating
found.setName("Bob");
database.update(found); // version auto-incremented
Collections (relationships)
Customer customer = database.find(Customer.class, 1);
List<Order> orders = customer.getOrders(); // Lazy loads automatically
Summary for AI Agents
When generating Ebean entity beans:
✅ DO:
- Use primitive
longfor @Id and @Version - Keep entities minimal (just fields + @Entity + @Id)
- Use @Version for concurrency control
- Use @WhenCreated/@WhenModified for audit trail
- Use List for collections, not Set
- Add constructors only if domain logic requires it
- Add getters/setters for all fields that application code needs to read or write
❌ DON'T:
- Use Long object for @Id/@Version
- Implement equals/hashCode
- Implement toString() with getters
- Use Set for @OneToMany/@ManyToMany
- Add unnecessary @Column annotations
- Add default constructors "just in case"
Result: Clean, readable, maintainable entity beans with full ORM functionality and zero boilerplate.
Related Documentation
- Entity Bean Best Practices:
/docs/best-practice/ - JPA Mapping Reference:
/docs/mapping/jpa/ - Ebean Extensions:
/docs/mapping/extensions/ - First Entity Guide:
/docs/intro/first-entity/