♻️ Idempotent Operations Patterns
Idempotent operations produce the same result regardless of how many times they're executed. This pattern is essential for reliable automation, infrastructure as code, and distributed systems where operations may need to be retried.
🎯 Core Principles
Deterministic Outcomes
Operations should have predictable results regardless of execution frequency.
| # ❌ Non-idempotent
create_user() {
useradd alice # Fails on second run if user exists
}
# ✅ Idempotent
create_user() {
id alice >/dev/null 2>&1 || useradd alice
}
|
State Inspection Before Action
Always check current state before making changes.
| # Generic idempotent pattern
ensure_state() {
local current_state=$(get_current_state)
local desired_state="$1"
if [ "$current_state" != "$desired_state" ]; then
apply_change "$desired_state"
fi
}
|
🔧 File System Operations
Directory Creation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | # ❌ Non-idempotent
mkdir /app/data
# ✅ Idempotent
mkdir -p /app/data
# More explicit version
ensure_directory() {
local dir="$1"
if [ ! -d "$dir" ]; then
mkdir -p "$dir"
echo "Created directory: $dir"
else
echo "Directory already exists: $dir"
fi
}
|
File Content Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | # ❌ Non-idempotent - always writes
echo "config=value" > /etc/myapp.conf
# ✅ Idempotent - only writes if different
write_config_idempotent() {
local file="/etc/myapp.conf"
local content="config=value"
if [ ! -f "$file" ] || [ "$content" != "$(cat "$file" 2>/dev/null)" ]; then
echo "$content" > "$file"
echo "Updated configuration: $file"
else
echo "Configuration unchanged: $file"
fi
}
|
File Permissions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | # ❌ Non-idempotent - always changes
chmod 644 /etc/myapp.conf
# ✅ Idempotent - only changes if needed
set_permissions_idempotent() {
local file="$1"
local desired_perms="$2"
local current_perms=$(stat -c "%a" "$file" 2>/dev/null)
if [ "$current_perms" != "$desired_perms" ]; then
chmod "$desired_perms" "$file"
echo "Set permissions $desired_perms on $file"
else
echo "Permissions already correct: $file"
fi
}
|
🛠️ Package Management
Package Installation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | # ❌ Non-idempotent - may fail or reinstall
apt-get install nginx
# ✅ Idempotent - checks before installing
install_package_idempotent() {
local package="$1"
if ! dpkg -l "$package" >/dev/null 2>&1; then
apt-get update
apt-get install -y "$package"
echo "Installed package: $package"
else
echo "Package already installed: $package"
fi
}
# Cross-platform version
ensure_package() {
local package="$1"
case "$(uname -s)" in
Linux*)
if command -v apt-get >/dev/null 2>&1; then
dpkg -l "$package" >/dev/null 2>&1 || {
apt-get update && apt-get install -y "$package"
}
elif command -v yum >/dev/null 2>&1; then
rpm -q "$package" >/dev/null 2>&1 || {
yum install -y "$package"
}
fi
;;
esac
}
|
<|fim_pad|>
Service Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | # ❌ Non-idempotent
systemctl start nginx
# ✅ Idempotent
ensure_service_running() {
local service="$1"
if ! systemctl is-active --quiet "$service"; then
systemctl start "$service"
echo "Started service: $service"
else
echo "Service already running: $service"
fi
}
# Ensure service is enabled and running
ensure_service() {
local service="$1"
# Enable service
if ! systemctl is-enabled --quiet "$service"; then
systemctl enable "$service"
echo "Enabled service: $service"
fi
# Start service
if ! systemctl is-active --quiet "$service"; then
systemctl start "$service"
echo "Started service: $service"
else
echo "Service already running: $service"
fi
}
|
🌐 Network Configuration
Firewall Rules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | # ❌ Non-idempotent - adds duplicate rules
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
# ✅ Idempotent - checks before adding
ensure_firewall_rule() {
local rule="$1"
if ! iptables -C $rule 2>/dev/null; then
iptables -A $rule
echo "Added firewall rule: $rule"
else
echo "Firewall rule already exists: $rule"
fi
}
# Usage
ensure_firewall_rule "INPUT -p tcp --dport 80 -j ACCEPT"
|
Hosts File Entries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | # ✅ Idempotent hosts file management
ensure_hosts_entry() {
local ip="$1"
local hostname="$2"
local hosts_file="${3:-"/etc/hosts"}"
local entry="$ip $hostname"
if ! grep -q "^$ip\s\+$hostname" "$hosts_file" 2>/dev/null; then
echo "$entry" >> "$hosts_file"
echo "Added hosts entry: $entry"
else
echo "Hosts entry already exists: $entry"
fi
}
|
🗃️ Data Operations
Database Records
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | # ✅ Idempotent database operations
ensure_database_record() {
local table="$1"
local condition="$2"
local insert_sql="$3"
# Check if record exists
if ! mysql -e "SELECT 1 FROM $table WHERE $condition" | grep -q "1"; then
mysql -e "$insert_sql"
echo "Inserted record into $table"
else
echo "Record already exists in $table"
fi
}
# Usage
ensure_database_record "users" "username='admin'" \
"INSERT INTO users (username, role) VALUES ('admin', 'administrator')"
|
Configuration Values
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | # ✅ Idempotent configuration management
ensure_config_value() {
local config_file="$1"
local key="$2"
local value="$3"
local pattern="^${key}\s*="
if grep -q "$pattern" "$config_file" 2>/dev/null; then
# Update existing value
sed -i "s/$pattern.*/$key=$value/" "$config_file"
echo "Updated $key in $config_file"
else
# Add new value
echo "$key=$value" >> "$config_file"
echo "Added $key to $config_file"
fi
}
|
🎨 Advanced Patterns
Transactional Operations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 | # Idempotent operations with rollback capability
idempotent_transaction() {
local operation_name="$1"
local check_fn="$2"
local apply_fn="$3"
local rollback_fn="$4"
if ! $check_fn; then
echo "Executing $operation_name..."
if $apply_fn; then
echo "Completed $operation_name"
else
echo "Failed $operation_name, attempting rollback..."
$rollback_fn
return 1
fi
else
echo "$operation_name already completed"
fi
}
# Example usage
check_user_exists() {
id alice >/dev/null 2>&1
}
create_user_safe() {
useradd alice
}
rollback_user_creation() {
userdel alice 2>/dev/null || true
}
idempotent_transaction "Create user alice" \
check_user_exists \
create_user_safe \
rollback_user_creation
|
Batch Idempotent Operations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | # Process multiple items idempotently
ensure_multiple_items() {
local items=("$@")
local ensure_fn="$1"
shift
for item in "${items[@]}"; do
$ensure_fn "$item"
done
}
# Usage
packages=("nginx" "postgresql" "redis")
ensure_multiple_items ensure_package "${packages[@]}"
services=("nginx" "postgresql" "redis")
ensure_multiple_items ensure_service "${services[@]}"
|
🧾 Summary Best Practices
Implementation Guidelines
✅ Always check current state before making changes
✅ Use descriptive feedback messages
✅ Handle edge cases gracefully
✅ Provide dry-run capabilities
✅ Log operations for audit trails
Common Patterns
- Check-Then-Act: Verify state before modification
- Compare-Then-Update: Only modify if different
- Existence-Then-Create: Only create if absent
- Status-Then-Change: Only change if needed
Anti-Patterns to Avoid
❌ Assuming operations are safe to repeat
❌ Ignoring existing state
❌ Producing side effects on repeated runs
❌ Failing silently on subsequent executions
🧠 Template Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | # Generic idempotent operation template
idempotent_operation() {
local name="$1"
local check_cmd="$2"
local apply_cmd="$3"
local success_msg="$4"
local skip_msg="$5"
if eval "$check_cmd"; then
echo "$skip_msg"
return 0
fi
if eval "$apply_cmd"; then
echo "$success_msg"
return 0
else
echo "Failed to $name" >&2
return 1
fi
}
# Example usage
idempotent_operation \
"create directory" \
"[ -d /app/data ]" \
"mkdir -p /app/data" \
"Created directory: /app/data" \
"Directory already exists: /app/data"
|
🧾 See Also