Skip to content

String concatenation anti-pattern

What is it?

The string concatenation anti-pattern occurs when developers manually concatenate strings to create composite keys instead of using helper functions. While this might seem simple at first, it leads to inconsistent formats, parsing errors, and difficult-to-debug issues.

Why is it a problem?

Manual string concatenation creates numerous issues:

  • Inconsistent Formats: Different developers use different separators (#, |, :, -)
  • Parsing Errors: Separators in data values break parsing logic
  • Escaping Nightmares: Special characters require complex escaping
  • Maintenance Burden: Changes require updating code in multiple places
  • Type Safety Loss: No compile-time validation of key structure
  • Query Errors: Inconsistent formats break query operations
  • Debugging Difficulty: Hard to identify which part of a key is wrong

The hidden danger

// What if the user's name contains a '#'?
const key = `USER#${userName}#ORDER#${orderId}`
// Result: "USER#John#Doe#ORDER#123"
// Is the name "John" or "John#Doe"? Impossible to parse!

Visual comparison

Example of the problem

❌ anti-pattern: manual string concatenation

// BAD: Manual concatenation with various separators
const userKey = `USER#${userId}`
const orderKey = `ORDER:${orderId}`  // Different separator!
const compositeKey = `${userId}|${orderId}`  // Another separator!

// BAD: No escaping of special characters
const userName = "John#Doe"  // Name contains separator!
const key = `USER#${userName}#PROFILE`
// Result: "USER#John#Doe#PROFILE"
// Parser thinks "John" is the user ID!

// BAD: Inconsistent ordering
const key1 = `${userId}#${orderId}`
const key2 = `${orderId}#${userId}`  // Reversed!
// Which is correct?

// BAD: No validation
const key = `USER#${undefined}#ORDER#${null}`
// Result: "USER#undefined#ORDER#null"
// Invalid key that will cause errors later

❌ common mistakes

// BAD: Different separators in same codebase
function createUserKey(id: string) {
  return `USER#${id}`
}

function createOrderKey(id: string) {
  return `ORDER:${id}`  // Different separator!
}

// BAD: Separator in data
const email = "user@example.com"
const key = `EMAIL#${email}#USER#${userId}`
// What if email contains '#'?

// BAD: Complex parsing logic
function parseKey(key: string) {
  const parts = key.split('#')
  // What if data contains '#'?
  // What if there are more or fewer parts than expected?
  // What if parts are in different order?
  return {
    type: parts[0],
    id: parts[1],
    // ... fragile parsing logic
  }
}

// BAD: No type safety
const key = createKey(orderId, userId)  // Arguments reversed!
// Compiler doesn't catch this error

The solution

✅ use helper functions

import { PatternHelpers } from '@ddb-lib/core'

// GOOD: Entity keys with consistent format
const userKey = PatternHelpers.entityKey('USER', userId)
// Returns: "USER#123"

// GOOD: Composite keys with automatic escaping
const compositeKey = PatternHelpers.compositeKey(['ORDER', userId, orderId])
// Returns: "ORDER#123#456"

// GOOD: Hierarchical keys
const hierarchicalKey = PatternHelpers.hierarchicalKey(['ORG', orgId, 'TEAM', teamId, 'USER', userId])
// Returns: "ORG#123#TEAM#456#USER#789"

// GOOD: Parsing with validation
const parsed = PatternHelpers.parseEntityKey(userKey)
// Returns: { entityType: 'USER', id: '123' }

✅ automatic escaping

// GOOD: Helper handles special characters
const userName = "John#Doe"  // Contains separator
const key = PatternHelpers.entityKey('USER', userName)
// Returns: "USER#John%23Doe" (# is escaped)

const parsed = PatternHelpers.parseEntityKey(key)
// Returns: { entityType: 'USER', id: 'John#Doe' }
// Correctly unescapes the value

✅ type safety

// GOOD: Type-safe key creation
interface UserKey {
  entityType: 'USER'
  id: string
}

function createUserKey(id: string): string {
  return PatternHelpers.entityKey('USER', id)
}

// GOOD: Validated parsing
function parseUserKey(key: string): UserKey {
  const parsed = PatternHelpers.parseEntityKey(key)
  if (parsed.entityType !== 'USER') {
    throw new Error(`Expected USER key, got ${parsed.entityType}`)
  }
  return parsed as UserKey
}

✅ consistent queries

// GOOD: Consistent key format enables reliable queries
const userPrefix = PatternHelpers.entityKey('USER', '')
// Returns: "USER#"

const allUsers = await table.query({
  keyCondition: {
    pk: { beginsWith: userPrefix }
  }
})

// All user keys follow the same format

✅ multi-attribute keys

import { 
  createMultiAttributeKey, 
  parseMultiAttributeKey 
} from '@ddb-lib/core'

// GOOD: Complex keys with multiple attributes
const gsiKey = createMultiAttributeKey({
  status: 'ACTIVE',
  priority: 'HIGH',
  createdAt: '2024-01-01'
})
// Returns: "status=ACTIVE&priority=HIGH&createdAt=2024-01-01"

// GOOD: Parse back to object
const parsed = parseMultiAttributeKey(gsiKey)
// Returns: { status: 'ACTIVE', priority: 'HIGH', createdAt: '2024-01-01' }

// GOOD: Partial matching for queries
const partialKey = createMultiAttributeKey({
  status: 'ACTIVE',
  priority: 'HIGH'
})

const results = await table.query({
  indexName: 'StatusPriorityIndex',
  keyCondition: {
    gsi1pk: { beginsWith: partialKey }
  }
})

Impact explanation

Bug prevention

Manual concatenation leads to bugs:

// ❌ Manual concatenation bug
const key1 = `USER#${userId}#ORDER#${orderId}`
const key2 = `USER#${userId}#ORDER#${orderId}`  // Looks the same...

// But if userId or orderId contains '#':
const userId = "123#456"
const key1 = `USER#123#456#ORDER#789`
// Parser sees: USER, 123, 456, ORDER, 789
// Expected: USER, 123#456, ORDER, 789
// BUG: Wrong user ID!

// ✅ Helper function prevents this
const key = PatternHelpers.compositeKey(['USER', userId, 'ORDER', orderId])
// Automatically escapes: "USER#123%23456#ORDER#789"
// Parser correctly extracts: ['USER', '123#456', 'ORDER', '789']

Maintenance cost

// ❌ Manual concatenation requires changes everywhere
// In 50 different files:
const key = `USER#${userId}#ORDER#${orderId}`

// Need to change separator? Update 50 files!
// Need to add escaping? Update 50 files!
// Need to change order? Update 50 files!

// ✅ Helper function: change once
// In PatternHelpers:
function compositeKey(parts: string[]): string {
  // Change implementation here
  // All 50 files automatically updated
}

Query reliability

// ❌ Inconsistent format breaks queries
// File 1:
const key1 = `USER#${userId}`

// File 2:
const key2 = `USER:${userId}`  // Different separator!

// Query fails to find all users
const users = await table.query({
  keyCondition: {
    pk: { beginsWith: 'USER#' }
  }
})
// Misses users created with 'USER:' format!

// ✅ Helper ensures consistency
const key = PatternHelpers.entityKey('USER', userId)
// Always uses the same format

Detection

While there's no automatic detector for this anti-pattern, code review can identify it:

Warning signs

Look for these patterns in code reviews:

// 🚩 Red flag: Template literals with separators
const key = `${type}#${id}`
const key = `${a}|${b}|${c}`
const key = `${prefix}:${value}`

// 🚩 Red flag: String concatenation
const key = type + '#' + id
const key = [a, b, c].join('#')

// 🚩 Red flag: Manual parsing
const parts = key.split('#')
const [type, id] = key.split(':')

// 🚩 Red flag: Multiple separator styles
const key1 = `USER#${id}`
const key2 = `ORDER:${id}`
const key3 = `ITEM|${id}`

// ✅ Green flag: Using helpers
const key = PatternHelpers.entityKey('USER', id)
const key = PatternHelpers.compositeKey(['ORDER', userId, orderId])

How to fix

Step 1: identify manual concatenation

# Search for manual concatenation patterns
grep -r "\\${.*}#\\${" src/
grep -r "\\.split('#')" src/
grep -r "\\.join('#')" src/

Step 2: replace with helper functions

// Before
const key = `USER#${userId}#ORDER#${orderId}`

// After
import { PatternHelpers } from '@ddb-lib/core'
const key = PatternHelpers.compositeKey(['USER', userId, 'ORDER', orderId])

Step 3: update parsing logic

// Before
const [type, userId, , orderId] = key.split('#')

// After
const parts = PatternHelpers.parseCompositeKey(key)
const [type, userId, , orderId] = parts

Step 4: add tests

import { PatternHelpers } from '@ddb-lib/core'

describe('Key handling', () => {
  it('should handle special characters', () => {
    const userId = 'user#with#hashes'
    const key = PatternHelpers.entityKey('USER', userId)
    const parsed = PatternHelpers.parseEntityKey(key)

    expect(parsed.id).toBe(userId)  // Correctly preserved
  })

  it('should create consistent format', () => {
    const key1 = PatternHelpers.entityKey('USER', '123')
    const key2 = PatternHelpers.entityKey('USER', '123')

    expect(key1).toBe(key2)  // Always the same
  })
})

Best practices

Do's

Always use helper functions for key creation ✅ Use parseEntityKey/parseCompositeKey for parsing ✅ Define key formats in one place (helper functions) ✅ Add validation to parsing functions ✅ Write tests for keys with special characters ✅ Document key formats in code comments

Don'ts

Never manually concatenate keys with template literals ❌ Never use different separators in the same codebase ❌ Never assume data doesn't contain separatorsNever skip escaping special characters ❌ Never parse keys with simple split() operations ❌ Never change key formats without migration plan

Summary

The Problem: Manual string concatenation for keys leads to inconsistent formats, parsing errors, and maintenance nightmares.

The Solution: Use helper functions from @ddb-lib/core that provide consistent formatting, automatic escaping, and type safety.

The Impact: Helper functions prevent bugs, reduce maintenance cost, and ensure query reliability.

Remember: Keys are the foundation of your DynamoDB data model. Don't build that foundation with fragile string concatenation. Use battle-tested helper functions that handle edge cases and maintain consistency.