Blog › ICP guides

4D developer on retainer: READ WRITE record lock not released on CANCEL, 4th Dimension developer, 4D v18 developer on monthly retainer

October 9, 2026 · ~15 min read

A 4D developer was maintaining a dental practice management application running on 4D v18 with a 4D Server on Windows Server 2019. The 4D Server hosted the practice database for a multi-dentist dental office with 12 front-desk workstations across two reception counters and a scheduling coordinator station. The application managed patient appointments, insurance billing, and clinical notes. A form method attached to the appointment scheduling sub-form was responsible for allowing front-desk operators to modify existing appointment slots: it called READ WRITE([Appointments]) to place the Appointments table into read-write mode for the current 4D process, then called LOAD RECORD([Appointments]) to load and lock the current appointment record for editing. The operator could then modify the appointment time, operator, or treatment type. Before saving, the form method called an internal validation subroutine to check whether the requested appointment slot was already occupied by another patient in the same operatory. When the conflict check found an occupied slot, the validation failure branch displayed a warning dialog with ALERT("This appointment slot is already taken. Please select a different time or operatory.") and called CANCEL to discard the edit and return the operator to the scheduling calendar view. The developer who wrote the form method placed READ ONLY([Appointments]) — which releases the record lock and returns the table to read-only mode for the process — only on the success path after SAVE RECORD([Appointments]). The CANCEL on the conflict failure path exited the sub-form without calling READ ONLY([Appointments]). In 4D’s process-based record locking model, the Appointments table record lock acquired by LOAD RECORD([Appointments]) is held by the 4D process until an explicit READ ONLY([Appointments]), UNLOCK RECORD([Appointments]), or process termination. Other front-desk workstations attempting to open the same appointment record for editing received 4D error −9997 “Record already in use by another user.” Record lock errors per day: 3–6 at peak morning and afternoon booking hours; 0 after READ ONLY([Appointments]) was added before CANCEL on the conflict failure path and on all other validation failure paths.

The root cause was the interaction between 4D’s process-based record locking model and the semantics of the CANCEL command in 4D form event handling. In 4D’s native database engine, record locking is managed at the process level, not at the transaction level: a process acquires a record lock when it calls LOAD RECORD([Table]) (or equivalently when the user opens a record for editing in a 4D form that is configured with the “modify current record” form property), and the lock is held by that process until explicitly released, the process terminates, or a CANCEL TRANSACTION releases it as part of an active 4D transaction. CANCEL in 4D form context does two things: it marks the form result as “cancel” (so that the calling form or method knows the user did not confirm the edit) and it discards any unsaved modifications to the current record’s fields. What CANCEL does not do is change the 4D process’s table access mode or release any record locks acquired by calls to READ WRITE or LOAD RECORD during the form’s lifecycle. The READ WRITE([Appointments]) call placed the Appointments table into read-write mode for the process; that mode persists until READ ONLY([Appointments]) is called or the process ends. The LOAD RECORD([Appointments]) call acquired an exclusive lock on the current appointment record; that lock persists until READ ONLY([Appointments]), UNLOCK RECORD([Appointments]), or the process ends. The CANCEL command, despite discarding the edit, did not affect either the read-write mode or the record lock. The form closed, the operator returned to the calendar view, and the 4D process continued running — still holding the Appointments record lock.

The invisibility in single-workstation development testing follows the universal multi-user lock bug pattern. The developer who built the Appointments form tested the scheduling conflict scenario on a development 4D instance with a single workstation. They created a test appointment slot, attempted to modify it to conflict with an existing appointment, confirmed that the conflict alert appeared and that the edit was discarded, and verified that the scheduling calendar displayed the original unchanged appointment. The test passed completely correctly: the edit was cancelled, the record was unchanged, and the operator experience was as designed. The 4D process’s continued ownership of the Appointments record lock was invisible because there was no second workstation attempting to access the same record. In production with 12 concurrent front-desk workstations running rapid back-to-back appointment bookings during morning rush, the collision became visible when a front-desk operator at station FRONT_DESK_B hit the conflict validation failure (triggering CANCEL without READ ONLY) and a second operator at FRONT_DESK_A or FRONT_DESK_C then attempted to open the same appointment for a different modification (rescheduling, confirming the patient, or adding a note). The second operator received error −9997 with no explanation of which workstation held the lock or why the record was showing as in-use by another user.

4th Dimension: from the 1984 Macintosh to modern vertical-market applications

4th Dimension (4D) was created by Laurent Ribardière and first released in 1984 by ACI (Automatisation et Conceptualisation Informatique) in France, initially as a Macintosh-exclusive database application development tool. The 1984 launch coincided with the introduction of the original Apple Macintosh, and 4D was among the earliest applications to take full advantage of the Macintosh’s graphical user interface — providing a drag-and-drop form editor, a direct-manipulation report designer, and an integrated scripting language that made it possible for non-programmers with domain knowledge (dental office managers, legal secretaries, school administrators) to build functional database applications without deep programming expertise. ACI established ACIUS Inc. as its US subsidiary in California in 1985 to market 4D in the North American market, and 4D became one of the most widely used Mac database development tools through the late 1980s, with a strong installed base in France, Belgium, French Canada, the UK, the Netherlands, Australia, and the United States.

Cross-platform support (Windows) was added in 4D 3.0 (1991), and 4D v6.0 (1996) introduced the client-server architecture (4D Client / 4D Server) that remains the foundation of its current deployment model: a 4D Server process runs on a central server host, managing the 4D database file and executing server-side code; 4D Client workstations connect to the server over a network and run the application forms and local scripts. The client-server split was particularly important for multi-workstation deployments in vertical markets — dental offices, law firms, school districts — where the application needed to support 5 to 25 concurrent users without the complexity and cost of a separate database engine (SQL Server, Oracle, or MySQL). 4D embedded its own proprietary database engine, which provided record-level locking, relational integrity, and indexed queries without requiring a separate database administration layer.

4D v11 SQL (2007) added a native SQL layer alongside the classic 4D language, allowing queries and DML statements using SQL syntax against the same embedded 4D database. 4D v14 and later versions introduced significant modernization: the 4D Write Pro and 4D View Pro integrated word-processing and spreadsheet components, REST API server capabilities, and a new form editor. 4D v17 introduced ORDA (Object Relational Data Access), a new object-oriented data access model that allows 4D code to interact with the database using entity and entity selection objects rather than classic 4D language record commands — similar in concept to an ORM layer built directly into the 4D runtime. 4D v19 and v20 (current, 2024) continue the modernization trajectory with enhanced web server capabilities, JavaScript integration through the 4D Language API for web frontend development, and component-based architecture. Despite the modernization, a large portion of the 4D installed base — particularly in dental practice management (where 4D was the platform of choice for French-Canadian and European dental software vendors through the 1990s and 2000s), legal matter management, and school student information systems — continues to run on applications originally written in 4D v6 through v14 with classic 4D language, maintained on retainer by developers who specialize in the older API.

4D’s record locking model is fundamentally different from SQL transactional locking. In SQL databases, a row lock is tied to a transaction: the lock is acquired when a transaction modifies or explicitly locks a row, and released when the transaction commits or rolls back. In 4D’s native database engine, a record lock is tied to a 4D process: it is acquired when a process calls LOAD RECORD([Table]) with the table in READ WRITE mode, and released when the process calls READ ONLY([Table]), UNLOCK RECORD([Table]), the process ends, or (in some cases) CANCEL TRANSACTION as part of an active 4D transaction. A 4D process is a persistent execution context on the 4D Server: it is created when a user connects and typically lives for the duration of their 4D Client session — potentially hours or the full working day. A record lock held by a process is held for the lifetime of the process unless explicitly released by the code. This differs from a SQL transaction, which has a defined commit/rollback lifecycle. Developers who come to 4D from a SQL or ODBC-oriented background often initially assume that a record lock will be released when the edit form closes or when control returns to the calling form — the “scope” assumption that works in transaction-based systems does not apply to 4D’s process-based locking model.

4D record locking model: READ WRITE, LOAD RECORD, CANCEL, and process lifetime locks

4D’s classic record locking commands operate on a table-level access mode and a record-level lock, managed independently per 4D process. READ WRITE([Table]) sets the table to read-write mode for the current process — subsequent LOAD RECORD([Table]) calls will attempt to acquire an exclusive lock on each loaded record. READ ONLY([Table]) sets the table back to read-only mode for the current process and simultaneously releases any record locks the process holds on that table. LOAD RECORD([Table]) loads the current record from disk into the process’s memory buffer and, if the table is in read-write mode, acquires an exclusive lock on that record (returning an error if the record is already locked by another process). LOCK RECORD([Table]{; record}) explicitly acquires a lock on a specific record regardless of whether the table is in read-write mode. UNLOCK RECORD([Table]{; record}) releases the lock on a specific record without changing the table’s access mode. Locked([Table]) is a function that returns True if the current record is locked by another process — used to test lock availability before attempting a modification.

CANCEL in 4D’s form event model exits the current form (or sub-form) and marks the form result as cancelled, discarding any field modifications made in the current form event cycle. It has no effect on the process’s table access mode or any record locks held by the process. A correct form method that uses READ WRITE([Appointments]) and LOAD RECORD([Appointments]) must call READ ONLY([Appointments]) on every exit path where it does not call SAVE RECORD([Appointments]) and READ ONLY([Appointments]) together. This includes: every CANCEL path, every early return, every error-handling exit, and every conditional exit where the operator decides not to save. The safe pattern for a multi-exit form method is to use 4D’s ON ERR CALL error handler or to structure the exit logic with a shared flag that ensures READ ONLY([Appointments]) is called before returning to the caller regardless of which exit path is taken.

Diagnosing a 4D process record lock requires access to the 4D Server Administration window (available on the 4D Server machine or via 4D Remote Administration). The Processes page of the 4D Server Administration window shows all currently running processes: process name, user, workstation, state (running, waiting, sleeping), and current record lock status. A process holding a record lock is displayed with the locked table and record number. In the dental practice scheduling incident, the Administration window during a lock error shows: Process 12, user FRONT_DESK_B, workstation 192.168.1.24, state Sleeping (waiting for user input on the calendar form), locked record: [Appointments] record 847. The process is sleeping — the operator at FRONT_DESK_B has returned to the calendar view after the conflict alert and is unaware that their process still holds the lock. The system administrator can kill Process 12 from the Administration window to release the lock immediately, but without the code fix, the pattern will recur every time a scheduling conflict is detected during appointment modification.

Typical 4D developer retainer work and what it looks like in a work log

READ WRITE record lock not released on CANCEL is the canonical 4D invisible production record lock bug for form methods that use READ WRITE([Table]) and LOAD RECORD([Table]) with validation failure paths that call CANCEL. It appears in the work logs of nearly every 4D retainer engagement for multi-workstation dental practice management, medical practice management, legal matter management, and school scheduling applications built on 4D v6 through v18. The pattern is consistent across versions: the form method developer tests on a single-workstation development instance; the single-process test never exercises the concurrent lock contention scenario; the missing READ ONLY on the CANCEL path is never flagged by 4D’s development environment; the bug is invisible until production operation with 8–12 concurrent front-desk stations hits the collision window. The work log entry: “4D v18, database SmilePractice; form method 'Appointments Form.Validate Slot'; READ WRITE call at line 3: READ WRITE([Appointments]); LOAD RECORD call at line 4: LOAD RECORD([Appointments]); scheduling conflict validation at line 18: If (conflict_found) ALERT('Appointment slot is already taken') CANCEL End if — CANCEL without READ ONLY([Appointments]); process holding lock: Process 12 (FRONT_DESK_B, 192.168.1.24, state Sleeping, locked record: [Appointments] 847); other stations receiving error −9997: FRONT_DESK_A and FRONT_DESK_C attempting LOAD RECORD([Appointments]) record 847; record lock errors per day before fix: 3–6 at peak booking hours; fix: READ ONLY([Appointments]) added before ALERT + CANCEL on conflict failure at line 18; full form method audited for additional CANCEL paths — 2 more found and fixed (insurance verification failure at line 27 and duplicate patient warning at line 34); record lock errors per day after fix: 0; 2h.”

4D structure and form maintenance is the routine day-to-day 4D retainer task for vertical-market applications that have evolved organically since the 1990s and have accumulated schema changes, form modifications, and method refactors from multiple development generations. A common engagement for a school student information system built on 4D v14: the school district has added a new state reporting requirement that requires a new data field (a standardized test accommodation code) on the student record, a new input field on the student enrollment form, and a new summary report for the state education department. The retainer developer opens the 4D database in development mode (with exclusive access to the 4D database structure), adds the new field (ACCOMMODATION_CODE of type Alpha 8) to the Students table via the Structure editor, adds the new input field to the Enrollment form via the Form editor with appropriate data entry validation, writes a new project method for the state report query using 4D’s query commands (QUERY([Students]; [Students]Accommodation_Code#""), ORDER BY([Students]; [Students]LastName)), and tests the full workflow. Work log: “4D v14; database StudentInfo; added ACCOMMODATION_CODE field (Alpha 8) to Students table; added input field to Enrollment form with validation (allowed codes list: 504, IEP, EL, NONE); wrote project method ExportAccommodationReport generating CSV output for state SPED reporting portal; tested with 12-student sample; 4h.”

4D version migration is the highest-investment 4D retainer engagement for clients running on 4D v6, v8, or v11 who need to move to a supported version for continued compatibility with modern macOS or Windows operating system releases. The migration assessment begins with a method inventory: enumerate all project methods, form methods, trigger methods, and table methods; identify deprecated 4D language commands (4D v17+ deprecates a large set of classic commands in favor of ORDA equivalents); review all form methods for record locking patterns that may require updating in the new version’s form lifecycle model. The migration also requires validating that calculated fields, query commands (QUERY([Table]; [Table]Field = value)), and RELATE ONE / RELATE MANY relation traversal commands work identically under the new version’s database engine. Work log: “4D v11 → 4D v18 migration; database SmilePractice; method inventory: 347 project methods, 89 form methods, 12 trigger methods; deprecated command audit: 14 methods using deprecated APPLY TO SELECTION replaced with For each/End for each; 6 methods using deprecated QR (Quick Report) commands replaced with 4D Write Pro; record lock audit: 23 form methods using READ WRITE + LOAD RECORD reviewed; 3 instances of CANCEL without READ ONLY identified and fixed (including Appointments Form.Validate Slot); 4D v18 compatibility test: all 347 project methods executed against production data snapshot; 2 methods with v11 syntax incompatibilities fixed; full application tested on Windows Server 2019; migration completed; 28h across 4 sessions.”

Track 4D developer retainer hours without the status emails

When a 2-hour investigation traces 3–6 daily record lock errors in a dental scheduling system to an Appointments form method that calls READ WRITE([Appointments]) and LOAD RECORD([Appointments]) for appointment editing then exits via CANCEL on scheduling conflict without calling READ ONLY([Appointments]) — holding the Appointments record lock in the 4D process for the session lifetime, blocking other front-desk stations with error −9997 — the work log must name the form method, the READ WRITE and LOAD RECORD call sites, the CANCEL path that lacked READ ONLY, the process holding the lock, and the error rate before and after the fix. HourTab gives your 4D retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the missing READ ONLY on the failure branch. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks 4D developer retainer hours

4D record lock bugs caused by READ ONLY([Table]) missing on CANCEL paths are invisible in single-workstation development testing by the same mechanism that makes all multi-user lock bugs invisible when only one user exercises the form. The developer who writes the Appointments form method tests the scheduling conflict scenario on a single-workstation development instance. They attempt to book a conflicting appointment slot, confirm that the “Appointment slot is already taken” alert appears and that CANCEL correctly returns them to the scheduling calendar. The 4D process holds the Appointments record lock after the CANCEL, but on a single-workstation development instance there is no second process attempting to lock the same record, so no error −9997 occurs and the lock hold is never visible. In production with 12 concurrent front-desk workstations running rapid appointment bookings during morning rush, the collision is visible only when two workstations intersect on the same appointment record in a narrow time window: one workstation holds the record lock after a conflict validation failure, and a second workstation attempts to open the same record for a different modification within the same session. The frequency is low enough that it never occurs in development or pre-production testing — it surfaces only under the concurrent workload of a real production multi-workstation environment.

The work log entry that makes 4D record lock work auditable must name every element of the causal chain: the 4D version, the database name, the form method name, the READ WRITE and LOAD RECORD call sites, the validation failure path that lacked READ ONLY, the process holding the lock (from the 4D Server Administration window: process number, user name, workstation IP, state, locked table and record number), the other workstations receiving error −9997, the lock error frequency before and after the fix, and the audit result for additional CANCEL paths in the same method. A log that says “fixed record locking issue in scheduling form, 2h” is not auditable to a dental office manager, a practice administrator, or an IT consultant reviewing the change history. A log that names the form method, the exact CANCEL path that lacked READ ONLY, the 4D Server Administration evidence, and the before/after error rate is auditable, defensible, and builds the trust that sustains a long-term 4D retainer relationship. HourTab gives 4D developers a public retainer-hours URL they send to clients — dental practices, medical offices, law firms, school districts, and other organizations that run core business operations on vertical-market applications built in 4th Dimension and maintain them with a 4D specialist on monthly retainer.

The broader context for 4D retainer billing is that the READ WRITE record lock not released on CANCEL pattern is the 4D instantiation of the universal principle that any platform requiring explicit lock release demands that the developer match every lock acquisition with a corresponding release on every exit path. FileMaker developer retainers cover the closest equivalent in the Mac RAD database family: FileMaker’s own record locking semantics (though FileMaker uses a somewhat different model, locking records at the window level rather than the process level) with similar invisible-in-single-user-testing characteristics. Progress OpenEdge ABL developer retainers cover the Progress ABL equivalent: FIND FIRST EXCLUSIVE-LOCK without RELEASE leaving a record lock on the failure branch — the same class of invisible single-user-test / multi-user-production lock bug, same discovery mechanism (concurrent users receiving lock-wait errors). DataFlex developer retainers cover the DataFlex equivalent: an implicit record lock acquired by FIND in a multi-user DataFlex application that is not released on the failure branch — same silent lock acquisition, same invisible single-user test, same multi-user production error.

FAQ: 4D developer retainers

What does a 4D developer on retainer typically do?

A 4D developer on monthly retainer covers record lock audits (reviewing every form method and project method that calls READ WRITE([Table]) or LOCK RECORD([Table]) to confirm that every CANCEL path, every validation failure, and every early return calls READ ONLY([Table]) or UNLOCK RECORD([Table])); lock error diagnosis (identifying which 4D process holds a record lock via the 4D Server Administration window and tracing it to the form method LOAD RECORD call site); 4D form and method development (form editor, form events, project methods, trigger methods, 4D class definitions in v17+); 4D structure modifications (adding fields and tables, managing indexes and relations in the Structure editor); 4D version migration (v6/v8/v11/v14 to v18/v19/v20, deprecated command replacement, ORDA migration); and vertical-market ISV application maintenance for dental, medical, legal, school, and retail applications built on 4D.

What 4D record lock debugging work is most commonly underlogged?

Record lock hold bugs caused by READ ONLY([Table]) missing on CANCEL paths are the most systematically underlogged 4D retainer work. The pattern: form method calls READ WRITE([Appointments]) + LOAD RECORD([Appointments]); developer tests on single-workstation development instance (no concurrent lock contention); production validation failure triggers CANCEL without READ ONLY([Appointments]); 4D process holds Appointments record lock for session lifetime; other workstations receive error −9997 “Record already in use by another user.” The work log must name the form method, the READ WRITE and LOAD RECORD call sites, the CANCEL path lacking READ ONLY, the locking process (from 4D Server Administration: process number, user, workstation, locked record), and the error frequency before and after the fix.

What are typical 4D developer retainer rates?

Entry-level 4D developers with experience in basic 4D language syntax, form event handling, and table management typically bill at $65 to $105 per hour. Mid-level 4D developers with experience in 4D record locking (READ WRITE / READ ONLY / LOCK RECORD / UNLOCK RECORD / Locked function), transaction management (START TRANSACTION / VALIDATE TRANSACTION / CANCEL TRANSACTION), and 4D Server administration typically bill at $95 to $160 per hour. Senior 4D developers with deep knowledge of 4D Server process architecture, 4D ORDA (v17+), 4D REST server, v19/v20 class-based programming, and vertical-market application architecture typically bill at $140 to $240 per hour. Monthly retainer ranges: $2,200 to $4,000/month for advisory and record lock audits; $3,500 to $7,000/month for active 4D application maintenance and 4D Server administration.

What should a 4D developer retainer agreement include?

A 4D developer retainer agreement should specify: 4D version (v14, v17, v18, v19, or v20 — ORDA, new form editor, and class-based programming availability differ by version); whether the retainer developer has 4D Server Administration access for process monitoring and record lock diagnosis; whether the retainer covers a record lock audit (reviewing all form methods using READ WRITE + LOAD RECORD for missing READ ONLY on every CANCEL and failure path); whether the retainer covers 4D structure modifications requiring exclusive database file access; the scope of the vertical-market application (dental, medical, legal, school SIS, or retail POS — domain-specific data integrity requirements); and whether the retainer includes 4D version migration scoping for clients on unsupported older versions.

How should 4D developer retainer hours be logged?

Log each 4D retainer session with the 4D version, database name, form or method name, specific issue, and before/after metric. For READ WRITE lock not released on CANCEL: 4D version (v18), database (SmilePractice), form method (Appointments Form.Validate Slot), READ WRITE call site (line 3), LOAD RECORD call site (line 4), validation failure path lacking READ ONLY (scheduling conflict check at line 18: If (conflict_found) ALERT(...) CANCEL End if without READ ONLY([Appointments])), process holding lock (Process 12, FRONT_DESK_B, 192.168.1.24, Sleeping, locked record [Appointments] 847), other stations receiving −9997 (FRONT_DESK_A and FRONT_DESK_C), record lock errors per day before fix (3–6), fix (READ ONLY([Appointments]) added before ALERT+CANCEL at line 18; 2 additional CANCEL paths fixed at lines 27 and 34), errors per day after fix (0), hours (2h). For version migration: source/target version, deprecated commands replaced, record lock audit count and fixes, ORDA migration scope, hours.