One day, you join a Laravel project.

You clone the repository, run:

composer install

Everything works perfectly.

A few days later, another developer joins the same project.

He runs the same command.

But suddenly…

“Why am I getting a different package version?”

You check the project and notice two important files:

composer.json
composer.lock

At first glance, they look similar.

Both are related to Composer dependencies.

So what is the actual difference?

Let’s understand it through a simple story.

Meet composer.json

Think of composer.json as your project's shopping list.

You tell Composer:

“These are the packages my project needs.”

For example:

{
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.0"
}
}

This does not necessarily tell Composer the exact version it must install.

For example:

laravel/framework: ^12.0

means:

“Give me a compatible Laravel 12.x version according to this constraint.”

So Composer has some flexibility when resolving dependencies.

Then Who Decides the Exact Version?

Enter our second file:

composer.lock

Think of composer.lock as the final purchase receipt.

It records the exact package versions Composer resolved.

Something like:

laravel/framework → 12.35.1
laravel/sanctum → 4.2.0
symfony/console → 7.3.2

So the relationship is:

composer.json
↓
"What packages do I need?"
↓
Composer dependency resolution
↓
composer.lock
↓
"These exact versions were selected"

A Real-Life Example

Imagine you’re building a Laravel application today.

Your composer.json says:

"laravel/framework": "^12.0"

Today, Composer might resolve:

Laravel 12.30.0

A month later, Laravel releases:

Laravel 12.35.1

Now another developer clones the project.

Without composer.lock

Composer may resolve a newer compatible version:

12.35.1

Your machine:

12.30.0

Another developer:

12.35.1

Now you have:

“But it works on my machine!”

And the debugging begins.

What Happens With composer.lock?

Suppose the project already has:

laravel/framework → 12.30.0

recorded in composer.lock.

Another developer clones the project and runs:

composer install

Composer uses the lock file and installs the versions recorded there.

So:

Developer 1 → 12.30.0
Developer 2 → 12.30.0
Developer 3 → 12.30.0
Production → 12.30.0

Much more predictable.

The Most Important Difference

Here is the easiest way to remember it:

FilePurposecomposer.jsonDefines dependency requirementscomposer.lockStores exact resolved versions

Or even simpler:

composer.json  = What I want
composer.lock = What I got

What Happens When You Run composer install?

This is where many developers get confused.

When composer.lock exists:

composer install

Composer installs the versions recorded in the lock file.

So it is generally the command you use when setting up an existing project.

For example:

git clone project
cd project
composer install

Then your environment gets the dependency versions expected by the project.

What Happens When You Run composer update?

Now things become different.

When you run:

composer update

Composer looks at:

composer.json

and resolves the dependencies again.

It may discover newer versions that satisfy your version constraints.

Then Composer updates:

composer.lock

For example:

Before
Laravel → 12.30.0
composer update

After
Laravel → 12.35.1
The lock file changes because the dependency resolution changed.

A Common Laravel Scenario

Imagine your project contains:

"require": {
"laravel/framework": "^12.0",
"spatie/laravel-permission": "^6.0"
}

Your lock file might contain:

laravel/framework → 12.30.0
spatie/laravel-permission → 6.20.0

Now Spatie releases:

6.21.0

Your composer.json doesn't necessarily need to change because:

^6.0

already allows compatible 6.x versions.

Running:

composer update

could update the package in composer.lock.

But running:

composer install

will normally stick to the versions already recorded in the lock file.

Should composer.lock Be Committed to Git?

For most applications, yes.

For example, a Laravel application repository commonly contains:

app/
bootstrap/
config/
database/
resources/
routes/
composer.json
composer.lock

Commit both:

git add composer.json composer.lock
git commit -m "Update dependencies"

Why?

Because your team wants predictable dependency versions across:

Local
↓
Development
↓
Staging
↓
Production

Everyone should ideally be using the same dependency set.

What About PHP Packages?

There is an important distinction between a project/application and a reusable package/library.

For a Laravel application:

composer.lock

is normally committed.

For a reusable PHP package, the lock file is generally not committed as part of the package’s published dependency contract, because consumers need Composer to resolve dependencies in the context of their own projects.

This distinction is important when working with packages published to platforms such as Packagist.

Does composer.lock Exist Automatically?

It is usually generated when Composer resolves and installs dependencies for a project.

For example:

composer install

or:

composer update

can create/update the lock file depending on the project state and command.

A typical project might start with:

composer.json

Then after dependency resolution:

composer.json
composer.lock
vendor/

What Is Inside composer.lock?

The lock file contains much more detail than just package names.

It records information such as:

Package name
Version
Source
Distribution
Dependencies
PHP requirements
Hashes / metadata

A simplified example:

{
"name": "laravel/framework",
"version": "v12.30.0",
"require": {
"php": "^8.2"
}
}

The real lock file is much larger because Laravel itself depends on many other packages.

Laravel Has More Dependencies Than You Think

You may write:

"laravel/framework": "^12.0"

But Laravel depends on many other Symfony and PHP ecosystem packages.

So Composer creates a dependency tree:

Your Laravel App
|
└── Laravel Framework
|
├── Symfony Console
├── Symfony HTTP Foundation
├── Symfony Routing
├── Symfony Process
└── ...

composer.lock records the resolved dependency graph.

That’s one reason the file can become quite large.

A Mistake Developers Often Make

Imagine you pull the latest code:

git pull

and someone changed:

composer.json
composer.lock

You then run:

composer update

just because you want to install the new package.

That may update many other dependencies too.

You could unexpectedly upgrade packages that were already working correctly.

For an existing project, this is often safer:

composer install

when the lock file already represents the intended dependency set.

When Should You Use composer update?

Use it when you intentionally want to re-resolve or upgrade dependencies.

For example:

composer update

or a targeted update such as:

composer update laravel/framework

After reviewing and testing the changes, commit the updated lock file.

The Golden Rule

Remember these two commands:

composer install
↓
Use the locked dependency versionscomposer update
↓
Resolve dependencies again
↓
Update composer.lock

And these two files:

composer.json
↓
Dependency rules / requirements


composer.lock
↓
Exact resolved dependency versions

Final Thought

The easiest analogy is this:

composer.json is your recipe.

It says:

“I need Laravel, Sanctum, Spatie Permission, and PHP 8.2+.”

composer.lock is the exact ingredient list that was selected.

It says:

“This is the precise version of every ingredient that made the application work together.”

That’s why both files are important, but they serve completely different purposes.

When you’re working on a Laravel team project, understanding this difference can save you from one of the most common dependency problems:

“It works on my machine, but not on yours.”

So the next time you see:

composer.json
composer.lock

remember:

JSON defines the rules.
LOCK records the result.

Happy Coding! 🚀

— Mr. Chauhan