TimefoldAI / TimefoldAI/timefold-solver

Allow package-private planning domain classes and methods

Open
#2,160 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
1.8k
Forks
228
Avg merge
1d 13h
Merged PRs (30d)
46

Description

Problem

Timefold requires all planning domain classes, getters, and setters to be public. This is enforced by MemberAccessorValidator, but the underlying accessor layer already handles non-public access — making this restriction unnecessary.

The contradiction

The reflection-based accessor already calls setAccessible(true) and creates a MethodHandle that works fine with non-public methods:

// ReflectionBeanPropertyMemberAccessor.java
public ReflectionBeanPropertyMemberAccessor(Method getterMethod, AnnotatedElement annotatedElement, boolean getterOnly) {
    this.getterMethod = getterMethod;
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    this.getterMethod.setAccessible(true);                          // ← handles non-public
    this.getherMethodHandle = lookup.unreflect(getterMethod)        // ← creates fast MethodHandle
            .asFixedArity();
    // ...
}

At runtime, it invokes via MethodHandle, not raw Method.invoke():

public Object executeGetter(Object bean) {
    return getherMethodHandle.invoke(bean);  // MethodHandle — JIT-optimized to direct call
}

But MemberAccessorValidator rejects non-public members before the accessor ever gets a chance:

// MemberAccessorValidator.java — declaring class gate
private static void verifyDeclaringClassIsAccessible(Member member, String messagePrefix) {
    var declaringClass = member.getDeclaringClass();
    if (!Modifier.isPublic(declaringClass.getModifiers())) {
        throw new IllegalArgumentException(
                "%s is not accessible because its declaring class (%s) is not public."
                    .formatted(messagePrefix, declaringClass.getCanonicalName()));
    }
}
// MemberAccessorValidator.java — repeated across verifyFieldOrGetter, verifyIsVoidMethod,
// verifyIsPublicFieldOrHasReadMethod, verifyGetterSetterProperties
if (!Modifier.isPublic(method.getModifiers())) {
    throw new IllegalArgumentException(
            "%s is a getter method, but it is not public."
                .formatted(messagePrefix));
}

There's also a discovery-layer issue — ReflectionHelper uses Class.getMethod() which only returns public methods, so package-private getters are invisible before validation even runs.

What about Gizmo performance?

The Gizmo path generates bytecode with direct invokeVirtual instructions, which requires public access. However, the reflection path already uses MethodHandle — which the JVM JIT-compiles to the same direct call after warmup:

Path Runtime execution After JIT
Gizmo invokeVirtual bytecode direct call
Reflection (MethodHandle) MethodHandle.invoke() direct call (inlined)

For non-public members, falling back to the MethodHandle-based reflection path carries no meaningful performance penalty. The Gizmo implementor itself documents this constraint:

// GizmoMemberAccessorImplementor.java (line 353)
// The member MUST be public if not called in Quarkus
// (i.e. we don't delegate to the field getter/setter).
Why this matters

Package-private domain models are a standard Java encapsulation pattern. In modular codebases, it's common to keep domain classes package-private and expose only the API boundary publicly. Today you're forced to write:

// Everything must be public even though nothing outside this package needs access
public class Lesson {
    public Timeslot getTimeslot() { ... }
    public void setTimeslot(Timeslot timeslot) { ... }
}

This would enable:

class Lesson {
    Timeslot getTimeslot() { ... }
    void setTimeslot(Timeslot timeslot) { ... }
}
Scope

This is a small change — no behavior change for existing public classes, no performance impact, and the accessor infrastructure already supports it.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading MemberAccessorValidator and ReflectionHelper to trace where non-public classes and methods are rejected or missed. Then inspect ReflectionBeanPropertyMemberAccessor and GizmoMemberAccessorImplementor to understand the existing fallback constraints. Done means package-private planning classes, getters, and setters are discoverable and usable without changing behavior for public classes.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.