A custom object Project__c has thousands of related Task__c records. When a Project is deleted, you need to archive all related tasks to another object before deletion. How would you design this in Apex?

How to archive thousands of related Task__c records when a Project__c record is deleted, including Apex design, trigger strategy, async processing, and full code.
1. Problem Statement and Business Context
In many Salesforce implementations, a Project__c custom object acts as a parent for a large number of Task__c child records. Over time, a single project can accumulate thousands (or tens of thousands) of tasks representing work items, milestones, approvals, or operational steps.
The business requirement is:
When a Project__c record is deleted, all related Task__c records must be archived into another object (for example, Archived_Task__c) before deletion.
Key Challenges
- Salesforce governor limits
- Large data volumes
- Parent deletion lifecycle constraints
- Need for data preservation
- Bulk-safe execution
- Avoiding mixed DML or recursive logic
2. Why This Is Not a Simple Trigger
A naive approach might be:
- Use a
before deletetrigger on Project__c - Query all related Task__c records
- Insert them into Archived_Task__c
- Let deletion proceed
❌ This fails at scale, because:
- A single project may have more than 10,000 tasks
- Triggers have SOQL row limits (50,000)
- Insert limits may be exceeded
- Deleting multiple projects at once multiplies the problem
3. Correct Design Strategy
To handle thousands of child records safely, we must use asynchronous Apex.
Final Design Pattern
| Step | Description |
|---|---|
| 1 | Intercept Project deletion |
| 2 | Capture Project IDs |
| 3 | Use Queueable Apex (or Batch) to archive tasks |
| 4 | Insert archived records |
| 5 | Delete original Task__c records |
| 6 | Allow Project deletion to complete |
4. Architectural Overview
Objects Involved
Project__c→ ParentTask__c→ ChildArchived_Task__c→ Archive storage
Apex Components
- Project Trigger (
before delete) - Trigger Handler
- Queueable Apex class
- Test class
5. Why Queueable Apex (Not Batch)?
| Option | Pros | Cons |
|---|---|---|
| Trigger only | Simple | Governor limits |
| Future method | Async | No chaining, no monitoring |
| Batch Apex | Best for millions | Overkill for deletion |
| Queueable Apex | Async, scalable, chainable | Ideal choice |
✅ Queueable Apex is the best balance between simplicity and scalability.
6. Data Model: Archived_Task__c
The archive object should mirror important fields from Task__c.
Example Fields
| Field | Type |
|---|---|
Original_Task_Id__c | Text (18) |
Project_Name__c | Text |
Task_Name__c | Text |
Status__c | Picklist |
Assigned_To__c | Lookup(User) |
Completed_Date__c | Date |
Archived_On__c | Date/Time |
7. Trigger Design
Key Rule
Never do heavy processing inside a trigger.
The trigger should:
- Collect Project IDs
- Hand them to async processing
Project Trigger
trigger ProjectTrigger on Project__c (before delete) {
if (Trigger.isBefore && Trigger.isDelete) {
ProjectTriggerHandler.handleBeforeDelete(Trigger.old);
}
}
JavaScript8. Trigger Handler Class
Responsibilities
- Extract Project IDs
- Enqueue Queueable job
- Stay bulk-safe
ProjectTriggerHandler.cls
public class ProjectTriggerHandler {
public static void handleBeforeDelete(List<Project__c> projects) {
Set<Id> projectIds = new Set<Id>();
for (Project__c proj : projects) {
projectIds.add(proj.Id);
}
if (!projectIds.isEmpty()) {
System.enqueueJob(
new TaskArchiveQueueable(projectIds)
);
}
}
}
JavaScript9. Queueable Apex: Core Business Logic
This is where the real work happens.
Responsibilities
- Query related Task__c records
- Create Archived_Task__c records
- Insert archived records
- Delete original tasks
- Handle thousands of records safely
TaskArchiveQueueable.cls
public class TaskArchiveQueueable implements Queueable {
private Set<Id> projectIds;
public TaskArchiveQueueable(Set<Id> projectIds) {
this.projectIds = projectIds;
}
public void execute(QueueableContext context) {
// Step 1: Query related tasks
List<Task__c> tasks = [
SELECT Id,
Name,
Status__c,
Assigned_To__c,
Completed_Date__c,
Project__c
FROM Task__c
WHERE Project__c IN :projectIds
];
if (tasks.isEmpty()) {
return;
}
// Step 2: Prepare archive records
List<Archived_Task__c> archiveList = new List<Archived_Task__c>();
for (Task__c t : tasks) {
Archived_Task__c archive = new Archived_Task__c();
archive.Original_Task_Id__c = t.Id;
archive.Task_Name__c = t.Name;
archive.Status__c = t.Status__c;
archive.Assigned_To__c = t.Assigned_To__c;
archive.Completed_Date__c = t.Completed_Date__c;
archive.Archived_On__c = System.now();
archiveList.add(archive);
}
// Step 3: Insert archived records
insert archiveList;
// Step 4: Delete original tasks
delete tasks;
}
}
JavaScript10. Handling Very Large Data Volumes (Advanced)
If a single project may exceed 50,000 tasks, use Batch Apex instead.
Hybrid Strategy
- Trigger → Queueable
- Queueable → Batch Apex (only if task count > threshold)
This ensures:
- Zero governor limit risk
- Horizontal scalability
11. Transaction Safety Considerations
Why This Works
- Trigger executes first
- Queueable runs after commit
- Project deletion is not blocked
- Child data is safely archived
Data Integrity
- Tasks are archived before deletion
- Original Task IDs preserved
- Historical data retained forever
12. Bulk Deletion Handling
If users delete multiple projects at once:
- Trigger receives multiple records
- All project IDs collected
- Single Queueable job processes them together
- Efficient and governor-safe
13. Error Handling Enhancements (Optional)
Add Try-Catch
try {
insert archiveList;
delete tasks;
} catch (Exception e) {
System.debug('Archive failed: ' + e.getMessage());
}
JavaScriptProduction Enhancements
- Platform Events for failure logging
- Custom error object
- Retry queue mechanism
14. Unit Test Class
A strong solution must be fully testable.
ProjectArchiveTest.cls
@IsTest
public class ProjectArchiveTest {
@IsTest
static void testTaskArchivalOnProjectDelete() {
// Create Project
Project__c project = new Project__c(Name = 'Test Project');
insert project;
// Create Tasks
List<Task__c> tasks = new List<Task__c>();
for (Integer i = 0; i < 5; i++) {
tasks.add(new Task__c(
Name = 'Task ' + i,
Project__c = project.Id,
Status__c = 'Open'
));
}
insert tasks;
Test.startTest();
delete project;
Test.stopTest();
// Verify archived tasks
List<Archived_Task__c> archivedTasks =
[SELECT Id FROM Archived_Task__c];
System.assertEquals(5, archivedTasks.size());
// Verify original tasks deleted
System.assertEquals(
0,
[SELECT COUNT() FROM Task__c]
);
}
}
JavaScript15. Governor Limit Compliance
| Limit | Compliance |
|---|---|
| SOQL queries | 1 |
| DML statements | 2 |
| Heap size | Safe |
| CPU time | Async optimized |
| Rows processed | Scalable |
16. Why This Design Is Enterprise-Grade
This solution demonstrates:
- Proper trigger discipline
- Async processing knowledge
- Large-data handling
- Clean separation of concerns
- Strong test coverage
- Interview-ready architecture
17. Possible Enhancements
- Custom Metadata to control archival behavior
- Partial archiving based on task status
- Soft delete flag instead of physical deletion
- Platform Event logging
- Scheduled cleanup jobs
18. Final Summary
To archive thousands of Task__c records when a Project__c is deleted:
- Use a before delete trigger
- Delegate logic to a handler class
- Process child records asynchronously using Queueable Apex
- Archive data into a separate object
- Delete original child records safely
- Maintain bulk safety and governor compliance
This design is scalable, reliable, and production-ready, making it suitable for both enterprise Salesforce orgs and technical interviews.
Related Posts

How to Automatically create a follow-up Task when a Lead is converted

How You need to update a related child record whenever a parent record’s status changes, but only if the status is “Closed Won.” How would you design this in Apex?
