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). Getters/setters are optional, and if used they don't need JavaBeans naming conventions; no manual equals/hashCode implementation is needed Language: Java Framework: Ebean ORM
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) | Ebean can use field access directly. Add accessors when your API/design needs them; naming can be JavaBeans, fluent, or custom. | |
| 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 |
Use for optimistic locking. Highly recommended. | |
@WhenCreated |
Auto-timestamp creation time. Highly recommended. Use for audit trail. | |
@WhenModified |
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.
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.
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.
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
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"));
DB.save(order); // createdAt is automatically set by Ebean
// Modify
order.setTotalAmount(new BigDecimal("109.99"));
DB.update(order); // version incremented, modifiedAt updated automatically
// Check when modified
System.out.println(order.getModifiedAt()); // Current timestampUse 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
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
Use this when: External code accesses this entity's fields.
@Entity
public class Employee {
@Id long id;
@Version long version;
String firstName;
String lastName;
BigDecimal salary;
public long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public BigDecimal getSalary() {
return salary;
}
public void setSalary(BigDecimal salary) {
this.salary = salary;
}
}Note: This is valid but verbose. Most Ebean code doesn't use getters/setters. Only add them if needed by your API design.
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.
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.
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.
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.
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 + '\'' +
'}';
}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.
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.
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
Start minimal, add what you need:
Step 1: Minimal
@Entity
public class BlogPost {
@Id long id;
String title;
String 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;
}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;
}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;
}
}Each step adds only what's necessary. No getters/setters needed unless your design requires them.
Customer customer = new Customer();
customer.setName("Alice");
DB.save(customer); // id auto-generatedCustomer found = DB.find(Customer.class, 1);
System.out.println(found.getName());found.setName("Bob");
DB.update(found); // version auto-incrementedCustomer customer = DB.find(Customer.class, 1);
List<Order> orders = customer.orders; // Lazy loads automaticallyWhen 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 only if your API requires them
❌ 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 getters/setters "just in case"
- Add default constructors "just in case"
Result: Clean, readable, maintainable entity beans with full ORM functionality and zero boilerplate.
- 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/