Use one solved project from several threads without corrupting its state: run what-if edits in parallel, or let many request threads edit the same plan.
A Project isn't synchronized, and neither is JsonProject. Every call can write internal state, even copy(), which marks the data as shared on both sides. Two patterns are safe:
Give each thread its own copy, made on the thread that owns the project.
Route every call on one shared project through a lock.
Make one copy per task on the thread that owns the project, before you hand any work to the pool. copy() is O(1): the copies share data until one of them is edited.
List<Project> drafts =newArrayList<>();for(int i =0; i < options.length; i++){
drafts.add(project.copy());}
Give each worker exactly one copy. The worker can edit and solve it freely. Its first edit deep-copies the data, so the other copies and the original don't change.
Don't call anything on the original while the workers run. If you need the original, take the lock from the next section.
Treat every ScheduleResult as read-only. A copy shares the cached result with its original until one of them is edited, so two threads can hold the same object. Call deepClone() before you change one.
Wrap the project in a class and take one lock for every call, reads included. solve(null, …) stores its cached result and copy() writes sharing flags, so no call counts as a pure read.
Keep apply and the solve(null, …) that follows it in the same locked block. Otherwise another thread can apply its own edit in between, and the result covers both edits.
Hand out snapshot() copies for long work such as level or suggestAssignments, so the lock isn't held while it runs.
The full example loads a two-task plan into a SharedPlan, takes three snapshots, and stretches build to a different length on each one in a thread pool:
importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.Future;importorg.dhtmlx.plen.*;publicclassShareProject{// One project shared by many threads: every call goes through the lock.
staticclassSharedPlan{privatefinalProjectproject=newProject();synchronizedScheduleResultload(ProjectDatadata){return project.solve(data,newSolveOptions());}synchronizedScheduleResultedit(SchedulingChangeschanges){
project.apply(changes);return project.solve(null,newSolveOptions());}synchronizedProjectsnapshot(){return project.copy();}}staticSchedulingChangesstretch(Stringtask,doubleduration){var changes =newSchedulingChanges();
changes.tasks.update.add(newTaskUpdate(Id.str(task)).withDuration(Patch.of(duration)));return changes;}publicstaticvoidmain(String[]args)throwsException{var monday =Moment.fromYmdHm(2026,8,10,0,0);var data =newProjectData().withConfig(newSchedulingConfig().withUseCalendars(false)).withTasks(List.of(newTaskData(Id.str("design")).withStartDate(monday).withDuration(2.0),newTaskData(Id.str("build")).withStartDate(monday).withDuration(3.0))).withLinks(List.of(newLinkData().withId(Id.str("l1")).withSource(Id.str("design")).withTarget(Id.str("build"))));var plan =newSharedPlan();
plan.load(data);double[] options ={4.0,6.0,8.0};List<Project> drafts =newArrayList<>();for(int i =0; i < options.length; i++){
drafts.add(plan.snapshot());}ExecutorService pool =Executors.newFixedThreadPool(options.length);try{List<Future<String>> results =newArrayList<>();for(int i =0; i < options.length; i++){Project draft = drafts.get(i);double duration = options[i];
results.add(pool.submit(()->{
draft.apply(stretch("build", duration));ScheduleResult result = draft.solve(null,newSolveOptions());Mutation build = result.updates.stream().filter(u-> u.id.equals(Id.str("build"))).findFirst().orElseThrow();return"build = "+ duration +" days: ends "+ build.endDate;}));}for(Future<String> result : results){System.out.println(result.get());}}finally{
pool.shutdown();}ScheduleResult accepted = plan.edit(stretch("build",4.0));System.out.println("shared project updates: "+ accepted.updates.size());}}
design ends on August 12, so build starts there in every copy. The last line comes from the shared project. Its baseline still has build at 3 days, which is why the 4-day edit reports one update.