Mahamudul Hasan Rubel
HomeBlogCoursesAboutProjectsSkillsExperiencePhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • Blog
  • Courses
  • About
  • Projects
  • Skills
  • Experience
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

Subscribe to the newsletter

Get new articles and course lessons delivered to your inbox. No spam, unsubscribe anytime.

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
Lesson 37 of the Intermediate Laravel: Real-World Application Patterns course
LaravelJune 26, 20263 min read

Database Migrations Best Practices: Schema & Data Integrity

Master database migrations by writing reversible schema changes, handling complex data migrations, and managing foreign key constraints in team environments.

Laravelmigrationsdatabaseschemabest-practicesphpbackend

Previously in this course, we discussed Database Transactions for Data Integrity in Laravel, which ensures that your application logic remains consistent during multi-step updates. While transactions protect your data at runtime, database migrations protect your application's evolution over time.

In a professional team environment, the database is the shared source of truth. If your migration strategy is fragile, you aren't just slowing down development—you're risking production outages.

Mastering Database Migrations in a Team Environment

Migrations are version control for your database. When multiple developers are working on the same project board, running php artisan migrate must be a predictable, idempotent process.

1. Writing Reversible Migrations

Every migration must have a corresponding down() method. If a deployment fails, you need to be able to roll back to the previous stable state instantly.

Avoid using DB::statement() for complex schema changes when the Schema builder provides native support. Native methods are more likely to be driver-agnostic and easier to reverse.

PHP
#6A9955">// Good: Reversible
public function up()
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->string('status')->default('pending');
    });
}

public function down()
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->dropColumn('status');
    });
}

2. Handling Data Migrations

Sometimes, a schema change requires a structural shift that involves existing data. For example, if you are splitting a name column into first_name and last_name, you cannot simply drop the old column immediately.

The pattern here is:

  1. Add the new columns (nullable).
  2. Migrate the data (usually via a separate migration or a temporary command).
  3. Validate the data.
  4. Drop the old column in a subsequent deployment.

Never perform heavy data manipulation inside a Schema migration if it involves thousands of rows; it will time out your deployment. Use a queued job or a custom Artisan command for the data movement.

3. Managing Foreign Key Constraints

Foreign keys are your last line of defense against corrupted data. Always define them, but be explicit about your onDelete behavior. Using cascade is convenient, but restrict or nullOnDelete is often safer for business-critical data like project tasks.

PHP
Schema::table('tasks', function (Blueprint $table) {
    $table->foreignId('project_id')
          ->constrained()
          ->onDelete('restrict'); #6A9955">// Prevents deleting a project with active tasks
});

Worked Example: Evolving the Project Board

In our running project, let's say we need to add a priority level to our tasks table. We need to ensure that existing tasks receive a default value without breaking the database.

  1. Create the migration: php artisan make:migration add_priority_to_tasks_table --table=tasks
  2. Define the schema change:
PHP
public function up()
{
    Schema::table('tasks', function (Blueprint $table) {
        #6A9955">// Add as nullable first, or provide a default
        $table->unsignedTinyInteger('priority')->default(1);
    });
}

public function down()
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->dropColumn('priority');
    });
}

Hands-on Exercise

In your project board, create a new migration to add a due_date column to the tasks table.

  1. Use the timestamp column type.
  2. Ensure the migration is reversible.
  3. Add a foreign key constraint to link tasks to a category_id (assuming you have a categories table), using onDelete('cascade').
  4. Run php artisan migrate, then php artisan migrate:rollback to verify your down() method works as expected.

Common Pitfalls

  • Modifying existing migrations: Never edit a migration file that has already been pushed to the repository. Once it's in production, it's immutable. Create a new migration to fix the issue.
  • Ignoring the down() method: If you leave it empty, you lose the ability to recover from a bad migration.
  • Hardcoded environment logic: Avoid using env() inside migrations. Use configuration files or default values; migrations should be environment-agnostic.
  • Assuming local state: Always assume your migration will run on a database that has different data than yours. Use nullable() or default values to avoid "Column cannot be null" errors.

Recap

Effective database management requires treating migrations as production-grade code. By writing reversible schema changes, handling data migrations as distinct steps, and enforcing schema integrity with foreign keys, you ensure that your team can deploy with confidence.

Up next: Job Chaining and Batching, where we will handle complex, multi-step asynchronous workflows for our project board.

Previous lessonLogging and MonitoringNext lesson Advanced Testing: Integration Tests
Back to Blog

Similar Posts

LaravelJune 25, 20263 min read

Understanding Database Migrations: A Laravel Beginner's Guide

Learn how to manage your database schema using Laravel migrations. Discover how to create, define, and run schema updates for your Task Manager project.

Read more
LaravelJune 26, 20264 min read

Handling Concurrency and Race Conditions in Laravel

Learn how to prevent data corruption in high-traffic applications by mastering database locks, atomic increments, and concurrency control in Laravel.

Part of the course

Intermediate Laravel: Real-World Application Patterns

intermediate · Lesson 37 of 58

  1. 1

    Architecting for Maintainability

    3 min
  2. 2

    Implementing the Service Layer

    3 min
  3. 3

    Repository Pattern Fundamentals

    3 min
Read more
LaravelJune 25, 20263 min read

Project Board Domain Modeling: Database Design and Eloquent

Learn how to design a scalable database schema for a project board, establish Eloquent relationships, and enforce data integrity with migration constraints.

Read more
  • 4

    Project Board Domain Modeling

    3 min
  • 5

    Advanced Eloquent Scopes and Accessors

    4 min
  • 6

    Service-Oriented Task Management

    3 min
  • 7

    REST API Fundamentals with Sanctum

    3 min
  • 8

    Resource Controllers and API Responses

    3 min
  • 9

    Handling API Validation and Form Requests

    3 min
  • 10

    Implementing Middleware for API Security

    4 min
  • 11

    Database Transactions for Data Integrity

    3 min
  • 12

    Error Handling and Global Exceptions

    3 min
  • 13

    Introduction to Laravel Events and Listeners

    3 min
  • 14

    Asynchronous Processing with Queues

    4 min
  • 15

    Job Chaining and Batching

    3 min
  • 16

    Feature Testing Fundamentals

    4 min
  • 17

    Mocking Services and Repositories in Tests

    3 min
  • 18

    Testing Events and Jobs

    3 min
  • 19

    Database Factories and Seeding

    3 min
  • 20

    API Versioning Strategies

    4 min
  • 21

    Advanced Request Filtering and Sorting

    3 min
  • 22

    Handling File Uploads in REST APIs

    3 min
  • 23

    Real-time Notifications with Broadcasting

    3 min
  • 24

    Using Observers for Model Lifecycle Hooks

    3 min
  • 25

    Implementing Policies for Authorization

    3 min
  • 26

    Customizing Authentication Guards

    3 min
  • 27

    Rate Limiting API Endpoints

    4 min
  • 28

    Eloquent Performance Optimization

    4 min
  • 29

    Caching Strategies for Performance

    4 min
  • 30

    Using Traits for Code Reuse

    3 min
  • 31

    Advanced Dependency Injection with Service Providers

    3 min
  • 32

    Command Line Tools with Artisan

    3 min
  • 33

    Scheduled Tasks and Cron Jobs

    3 min
  • 34

    Integrating Third-Party Services

    3 min
  • 35

    Handling Webhooks

    3 min
  • 36

    Logging and Monitoring

    3 min
  • 37

    Database Migrations Best Practices

    3 min
  • 38

    Advanced Testing: Integration Tests

    4 min
  • 39

    Testing API Authentication

    4 min
  • 40

    Code Quality and Static Analysis

    3 min
  • 41

    Project Structure for Large Applications

    3 min
  • 42

    Environment and Configuration Management

    3 min
  • 43

    Deploying Laravel Applications

    4 min
  • 44

    Database Indexing Strategies

    4 min
  • 45

    Using Value Objects

    4 min
  • 46

    Strategy Pattern for Business Rules

    3 min
  • 47

    Advanced Queue Monitoring

    3 min
  • 48

    Building a Search API

    3 min
  • 49

    Handling Concurrency and Race Conditions

    4 min
  • 50

    API Documentation with OpenAPI

    3 min
  • 51

    Testing with Test Doubles

    3 min
  • 52

    Implementing Multi-Tenancy

    4 min
  • 53

    Refactoring Legacy Code

    4 min
  • 54

    Using Middleware for Feature Flags

    3 min
  • 55

    Building Reusable Packages

    4 min
  • 56

    Performance Profiling

    3 min
  • 57

    Secure API Design

    3 min
  • 58

    Event Sourcing Concepts

    4 min
  • View full course