3

In decomposing a monolithic web application into smaller services and by following the Strangler Fig pattern, I'm in the middle of a problem for which I can't find a practical solution. There is a web app written in PHP and I'm taking steps to:

  1. Move highly cohesive code into its own service (more precisely a docker container later and for now I wouldn't want to refactor a lot or rewrite significant parts of the application, which is huge).

  2. Move business logic from controllers and other places to a service / repository layer under a known namespace (this is a clean up step)

  3. Modify all old code to invoke new code (it will be like $this->newService->method(...$args))

  4. Then bring up a docker container in the same network which handles the requests from step 3. (those $this->newService->... calls will be remote calls but the code is not remote. I just added an extra network call here.)

Now the problem I'm facing is that I see different code that changes the model state (by model I mean M as in MVC) and then passes it to some other methods, they make their own changes on the model-object and then the changes are finally committed and written to the database.

But since I'm delegating all jobs to the new service, it would be like this:

$this->newService->methodA(123); // this selects a record from a MySQL table, performs some work and commits those changes
$this->newService->methodB(123); // this does the same thing

So with this new service I will have two selects and two updates, but in the monolithic application, there is no such repetition:

$model = Model::getByID(123);
// these are local calls
$this->serviceOne->methodA($model);
$this->serviceTwo->methodB($model);
$model->save();

My question is, what should be done in this case to prevent multiple database reads and writes? I am considering building a caching layer in order to persist model state and in that way avoid multiple individual reads and writes to the database and then later all at once commit all accumulated model changes to the database, but I can't figure if that is the right solution or not. I'm literally lost.

2
  • I don't really get the question. Your services doing something on the model. After this is done, your models get saved. These steps can be put in a sequence, so whats the problem?
    – Jim Panse
    Jan 12, 2021 at 7:44
  • 1
    @JimPanse Yes, but those are not a few steps and I do not want to do lots of refactors and more importantly those may not be sequential but be somewhere in the same lifecycle.
    – revo
    Jan 12, 2021 at 11:19

3 Answers 3

4

Each Service Owns its Own Data.

and it corollary...

Each Service passes the Callers data back to the Caller

So you have a choice.

  • Break your model up
  • Pass your model around

Break your model up so that the pieces of interest to Service A are owned by Service A, and similarly those owned by Service B are owned by Service B. The only data passed around is the identifier that links these two different views.

Think of it like a distributed ECS (Entity Component System). It is still a single Entity but each Service (System) owns its own view of the entity (Component). This works best when the components only share entity identifying information.

For example: An e-commerce site is backed by three services (for purchasing): Picking, Packing, Shipping. The Picking service wants to know about what was bought and how many so that it can be picked from stock, the packing service only cares about the picking bin so that it can parcel these items, and the shipping service cares only about the delivery address and the parcels. Each services perspective is linked by an order id.

You will need to rewrite your code so that you offload all responsibility to the service, no micromanaging. A single call with all the high-level information and trust that it will do all of the low-level bits like database management.


Alternately treat your object as unowned by the services. In this view you provide the object (or some facsimile) to the service, it applies its Trade Secret changes to the object and returns it back (or some facsimile). Its then up to the caller to integrate those changes back into the object passed out.

Think of this like a Orchestrated Pipes and Filters architecture. Your program provides the orchestration, and pipes. The Services are the Filters. This works best with a durable and general data-structure that doesn't change often, like picture files (or in the example below shirts).

For example: dry cleaners, you pass them your shirt. You don't know what they did to get the stain out, but its gone and now, so you can hand it to a seamstress to shorten those sleeves. But maybe the dry-cleaners or the seamstress returned rags instead, which can be handled by the garbage man.

1
  • It is often forgotten that the microservice architecture works when the microservices have their own data store. If you can't do that, then my humble opinion is that it would be better not to break that part off the monolith (at least for now).
    – Rad80
    Feb 9, 2021 at 11:56
2
+50

Option 1 create a separate service C with cache

The cache could be a simple key-value store like Redis that stores the Id, model, commitFromA, commitFromB per record. In each cycle, a transaction Id is generated and used as a key column in the cache.

  1. When $this->newService->methodA(123); is called, a read request is sent to service C, service C looks up the record with the current transaction Id from cache and returns (if not exists, it reads from SQL database and updates the cache).
  2. With the model data, methodA processes data and sends a write request to service C to update the cache.
  3. When $this->newService->methodB(123); is called, same read as step 1.
  4. With the model data, methodB processes data and sends a write request to service C to update the cache.
  5. The update method in service C verifies both commitFromA and commitFromB are filled out, then updates the SQL database, and then clears the record with current transaction Id from the cache.

Option 2 create database view for each service

Similar with option 1 --- you still need service C to synchronize the operations from methodA and B by transaction Id. The only difference is each service has its own database view so methodA and B reads from their local services respectively. The commit updates are sent to service C to a cache (in which it stores Id, commits only).

The pros for this options is to loose the coupling between service and database, so the schema change from one service does not affect other services.

An alternative way of using cache in service C, is to apply Saga pattern that records the transactions from methodA and B. The advantage of this way is failure handling since it allows undoing the changes if one of the methods in A or B fails. But this could be an overkill as you need a queue, and if you only have 2 methods that commit the change, the cache can be used to execute the compensating transactions to undo the changes too.

1

It sounds like you want to put a few operations into a transaction.

// Begin transaction
$myDB->beginTransaction(); // Not sure what your code will look like for this.
$model = Model::getByID(123);
// these are local calls
$this->serviceOne->methodA($model);
$this->serviceTwo->methodB($model);
// $model->save(); // Commit now happens after all operations have succeeded.
$myDB->commitTransaction(); // Again, not sure what syntax/operations you have available.
// You'll probably want to have a roll-back in an error-handling section as well
if ($errors)
{
    $myDB->rollback();
}

Now, you say that methodA and methodB will actually perform a few operations and then do a commit. So you might also need to adjust these methods to make the commit optional. Maybe pass a "commit flag", so the method knows if it should run in its own transaction, or in a larger transaction:

$doCommit = false;
$this->serviceOne->methodA($model, $doCommit);
5
  • 1
    The problem with $this->serviceOne->methodA($model, $doCommit); is that I don't know how to persist $model state in that service so if I have another call i.e. $this->serviceOne->methodB(.... I fetch the last state and work on it.
    – revo
    Jan 7, 2021 at 21:50
  • @revo: If the call to methodA and methodB are within the same transaction, then the database has already persisted the change for you, so you don't need to do anything else. But if you don't know in advance where your transactions boundaries may be, that does make things more complicated. You could introduce a caching layer, but what happens if someone tries to read an object after methodA completes and before methodB? Should the other reader get data from the cache or the database? Jan 7, 2021 at 21:56
  • Since it is in the same lifecycle then I guess it should read the last state from service not the one from database otherwise it should explicitly ask for it e.g. $readFromDB = true.
    – revo
    Jan 7, 2021 at 22:01
  • @revo Then maybe a caching system is better than transactions, for you? Jan 7, 2021 at 22:21
  • When it comes to objects and their states in PHP and unknown conditions that I have not seen beforehand while maintaining in a network round trip I can't tell if caching is the best and if it is what kind of store should be used? because I know there is XA transactions in MySQL which acts globally so I can utilize them and I'm aware of APCu and serializing an object in PHP for caching purposes but I need an advise on that.
    – revo
    Jan 7, 2021 at 22:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.